Перейти к основному содержимому

Выражения

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

ГдеНа что отвечает
if=участвует ли <gen>, <line>, <case> или <data> в этой строке
filter=из каких участников пула эта строка может тянуть
expr=ЗНАЧЕНИЕ колонки formula
параметр распределениязначение mean, sd, lambdaна этой строке

Первые два берут ответ как «да/нет» и выбрасывают значение; последние два его оставляют. Те же операторы, те же функции, те же имена для тех же колонок — именно это не даёт условию и вычисленной колонке начать понимать одни и те же слова по-разному.

<sequence name="Zone">
<gen if="Country in [US, CA, MX]" type="text" value="NAFTA"/>
<gen if="Country in [FR, DE]" type="text" value="EU"/>
<gen type="text" value="ROW"/>
</sequence>
<sequence name="Handling">
<gen if="Weight > 20" type="text" value="freight"/>
<gen if="_count % 2 == 0" type="text" value="courier-even"/>
<gen type="text" value="parcel"/>
</sequence>
./run shipping.tdc
US NAFTA 2kg parcel
FR EU 14kg courier-even
CA NAFTA 7kg parcel
DE EU 30kg freight
MX NAFTA 5kg parcel
JP ROW 22kg freight

У последнего <gen> в каждой последовательности нет if=, поэтому он забирает всё, что не подошло условиям выше, — та же форма, что и else.

Значения

Вы пишетеЧто это значит
Countryзначение, которое эта последовательность дала на этой строке
Person.Emailполе составной последовательности
Gender.Male«сейчас Gender равен Male?» — читается как parent="Gender.Male"
Maleголое слово: имя, которое не является последовательностью, — это текст
42, 1.5число
'текст'строка в кавычках, когда в тексте пробелы или он похож на имя
_count, _lastвстроенное — номер строки, признак последней

Голое слово — это то, что позволяет писать Gender == Male без кавычек. Оно же означает, что опечатка сравнивается сама с собой и молча ни с чем не совпадает, — поэтому неизвестное имя справа от точки даёт TDC193, а не проходит.

Целые числа

Double держит каждое целое до 2⁵³, а дальше начинает их пропускать — и выражение, построенное на одних double, отвечает так:

что double говорит о двух разных числах
9007199254740993 == 9007199254740992   true
9007199254740993 -  9007199254740992   0

И то и другое неверно, и неверно молча — а для генератора данных это худший вид ошибки: прогон завершается, файл выглядит нормально. Поэтому операнд, который является целым, и несётся как целое, а double становится только когда его об этом попросят:

литерал без точки и экспонентыостаётся целым
колонка, значение которой читается как цифрысравнивается как целое с другим целым
+ - * % над двумя целымиостаются целыми
abs round floor ceil trunc над целымостаются целыми: округление целого — это оно само, какого бы размера оно ни было
min max sum, пока все аргументы целыеостаются целыми
/всегда с плавающей точкой — деление не замкнуто на целых
всё, что уходит в sqrt, log, sinстановится double: у них точного ответа нет

Домен — знаковые 64 бита, тот же, что у слоя compute: это самое широкое целое, которое все пять реализаций держат сами. РЕЗУЛЬТАТ арифметики за его пределом — отказ, теми же словами, что у compute, а не тихое сползание обратно в плавающую точку:

tdcv2 ledger.tdc
tdcv2: integer overflow: 9223372036854775808 is outside the signed 64-bit range

Отказ приходит от прогона, а не от check: оба слагаемых лежат внутри домена, за его пределом только ответ, и доказывать валидатору нечего.

ЛИТЕРАЛ шире домена — другое дело: это double. 1 / 0 > 100000000000000000000 — это способ написать «больше любого целого, которое мы держим», и он работает; плата в том, что два литерала, округляющиеся к одному double, равны между собой, поэтому 10000000000000000000 == 10000000000000000001истина. Если имелся в виду не число, а имя, пишите его текстом.

Один край стоит знать: −2⁶³ достижимо арифметикой, но не записывается литералом — в -9223372036854775808 минус применяется к величине на единицу больше самого большого положительного.

Операторы

ГруппаОператоры
сравнение== != === !== < > <= >=
логика&& || !
арифметика+ - * / %

+ складывает, когда обе стороны читаются как числа, а колонка всегда текст — поэтому Price + Delivery на двух дробных колонках это арифметика, а не склейка. Он склеивает только тогда, когда сторона и правда не число: 'a' + 'b' даёт ab. Чтобы соединить значения нарочно, поставьте их рядом там, где они печатаются: ${{First}} ${{Last}}. | принадлежность | in | | выбор | a ? b : c |

== спрашивает, одно ли это число; === — печатаются ли обе стороны одними и теми же символами. Каждая колонка — текст, поэтому эти два вопроса действительно разные: см. Сравнение и истинность, где заодно разобрано, что считается истиной для голого if="Flag".

% евклидов, и это не то, что делает ваш язык

-3 % 2 здесь 1. JavaScript, Java, C# и Rust отвечают −1; Python отвечает 1.

Отрицательный ДЕЛИТЕЛЬ — место, где расходится и Python: 7 % -3 здесь 1, а там −2. Результат никогда не бывает со знаком — он всегда в 0 … |делитель| - 1.

Причина не во вкусе. В слое compute уже был <mod>, и он уже отвечал 1, — так что %, взявший соглашение хозяина, заставил бы один движок давать два разных ответа на один вопрос в зависимости от того, к какому слою вы потянулись.

in берёт справа список и ничего кроме — список в любом другом месте даёт TDC259. Сравнение внутри такое же нестрогое, как у ==, поэтому текстовая колонка против списка числовых слов всё равно совпадёт.

<gen if="Country in [US, CA, MX]" .../> <!-- вместо трёх == через || -->

Функции

ФункцияБерётДаёт
abs(x)1модуль
ceil(x) floor(x)1вверх / вниз до целого
trunc(x)1к нулю — trunc(-7.5) это −7, а floor даёт −8
round(x)1ближайшее, половина от нуля
min(…) max(…)1 и большенаименьшее / наибольшее
len(s)1сколько символов
is_empty(s)1пуст ли текст
starts_with(s, p)2проверка префикса
ends_with(s, p)2проверка суффикса
contains(s, p)2проверка вхождения
lower(s) upper(s)1регистр
split(s, sep)2текст, разрезанный в список — см. Списки внутри строки
join(list, sep)2список обратно в текст
count(list)1сколько элементов
at(list, i)2i-й элемент, счёт с нуля
sum(list)1сумма — остаётся целой, пока целы все элементы
mean(list) median(list)1среднее и середина
stddev(list)1популяционное стандартное отклонение, делится на n
sqrt(x)1квадратный корень
pow(x, y)2x в степени y
exp(x)1e в степени x
log(x) log10(x)1натуральный / десятичный логарифм
sin(x) cos(x) tan(x)1тригонометрия, в радианах
asin(x) acos(x) atan(x)1обратные к ним, дают радианы
atan2(y, x)2угол точки (x, y), в диапазоне (−π, π]
sinh(x) cosh(x) tanh(x)1гиперболические функции
cbrt(x)1кубический корень — берётся и от отрицательных
expm1(x) log1p(x)1eˣ−1 и log(1+x), точные около нуля
log2(x)1двоичный логарифм — точен на степени двойки
asinh(x) acosh(x) atanh(x)1обратные гиперболические
hypot(x, y)2длина вектора, без переполнения по дороге
hash(n, salt)2воспроизводимое значение в [0, 1) от пары чисел
noise(t, scale, salt)3плавный дрейф: новое значение каждые scale строк, со сглаживанием
gauss(x, c, w)3колокол с центром в c: exp(-((x - c) / w)²)
clamp(x, lo, hi)3x, удержанный в [lo, hi]; при lo > hi побеждает потолок
lerp(a, b, t)3доля t пути от a к b; точен на обоих концах, вне отрезка экстраполирует
sign(x)1−1, 0 или 1
erf(x) erfc(x)1функция ошибок и её дополнение
gamma(x) lgamma(x)1Γ(x) и log |Γ(x)| — на случай переполнения Γ
beta(a, b)2Γ(a)Γ(b)/Γ(a+b)
digamma(x)1ψ(x), производная от log Γ
zeta(s)1дзета-функция Римана, для вещественного s
degrees(x) radians(x)1между двумя способами записать угол
prev(Column, initial)2эта колонка на строку назад — нужен mode="sequential", см. ниже

Операторы — сравнения, логические связки и арифметика — точные: собраны из того, что IEEE-754 задаёт однозначно, поэтому пять реализаций не могут о них разойтись. А ФУНКЦИИ из таблицы выше TDC считает сам — потому у каждой и стоит граница точности, а не обещание точного ответа.

Два правила, которые стоит знать, прежде чем на них опираться

round отправляет половину от нуля. round(0.5) это 1, а round(-0.5) это −1. JavaScript округляет половину к +∞, Python — к чётному, Java — вверх: три хозяина, три ответа, ни одного симметричного. TDC называет своё правило, чтобы колонка отрицательных вела себя как колонка положительных.

len считает кодовые точки. len("😀") это 1, а не 2, которые дал бы UTF-16, — но семейный эмодзи, собранный из нескольких кодовых точек, посчитается как несколько. Графемы были бы человеческим ответом, но им нужна таблица сегментации Unicode, которую не всякая реализация может нести, — поэтому побеждает переносимая единица. len("10") это 2: строковая функция читает аргумент как текст, а не как число.

Повторяемая случайность и формы, в которые её кладут

Пять функций из таблицы выше существуют ради одной задачи: построить величину, которая МЕНЯЕТСЯ так, как меняются настоящие данные, ничего при этом не разыгрывая. Каждая — это обычный расчёт над своими аргументами, поэтому одни и те же аргументы всегда дают один и тот же ответ, в любой из пяти реализаций.

hash(n, salt) — повторяемое значение из [0, 1) по паре чисел

То, за чем тянешься, когда каждой строке нужен свой коэффициент:

<tdc><env count="5" seed="w">
<sequence name="N"><gen type="increment" value="1"/></sequence>
<sequence name="Amp"><gen type="formula" decimals="4"
expr="1.4 * (0.94 + 0.12 * hash(N, 12.9))"/></sequence>
</env><block><line><data>${{N}},${{Amp}}</data></line></block></tdc>
1,1.3862
2,1.4128
3,1.4559
4,1.4518
5,1.4734

salt — то, чем различаются две колонки одной строки: hash(N, 1) и hash(N, 2) — несвязанные последовательности, так что одна строка может нести несколько независимых коэффициентов без второго зерна.

Функция заменяет шейдерный трюк — sin(n * 12.9898) * 43758.5453 минус целая часть, — к которому обычно тянутся. У нас этот трюк безопасен: TDC считает свой sin сам, и пять реализаций сходятся до последнего бита. Цена его — два трансцендентных вызова на строку, считаемых программно, и строка, которую никто не прочтёт. hash — целочисленная арифметика, и он говорит, что он такое.

noise(t, scale, salt) — плавный дрейф

Свежее значение каждые scale строк, между ними — сглаживание. Именно так и выглядит блуждающая базовая линия, и три модулированных синуса так не умеют: на 4096 отсчётах синусы кладут 74.6% мощности в три частотные корзины, где это кладёт 51.5%.

<sequence name="Drift"><gen type="formula" expr="0.3 * (noise(_count, 300, 7) - 0.5)"/></sequence>

Два свойства, на которые можно опираться, — оба точные, а не приблизительные:

  • В узле решётки значение И ЕСТЬ hash в этой точке. noise(k * scale, scale, salt) равен hash(k, salt) — теми же битами, а не «с точностью до ulp», — потому что сглаживание интерполирует как a * (1 - u) + b * u. Значит, граница ячейки непрерывна.
  • Середина ячейки — обычное среднее её концов.

scale, равный нулю, даёт деление на ноль и NaN — тот же ответ, что здесь даёт sqrt(-1). В колонке это не печатается, а вызывает отказ по имени: файл, полный NaN, о которых никого не предупредили, хуже прогона, который остановился.

gauss(x, c, w) — колокол с центром в c

exp(-((x - c) / w)²): единица в центре, спад в обе стороны, w задаёт ширину.

<tdc><env count="7" seed="w">
<sequence name="X"><gen type="increment" value="1"/></sequence>
<sequence name="G"><gen type="formula" decimals="4" expr="gauss(X, 4, 1.5)"/></sequence>
</env><block><line><data>${{X}},${{G}}</data></line></block></tdc>
1,0.0183
2,0.1690
3,0.6412
4,1.0000
5,0.6412
6,0.1690
7,0.0183

Это не плотность вероятности — она не нормирована к единичному интегралу. Это ФОРМА, на которую умножают что-то другое.

clamp(x, lo, hi) — удержать значение в границах

Отбойник для всего, что накапливается. Если lo больше hi, побеждает потолок: clamp(x, 9, 2) равен 2 при любом x. Порядок важен, и он оговорён, а не отдан на волю того, какое сравнение реализация выполнит первым: однажды эталон и четыре порта об этом не сошлись, и поймала это общая фикстура.

lerp(a, b, t) — доля t пути от a к b

Точно попадает в оба конца: lerp(10, 20, 0) — ровно 10, lerp(10, 20, 1) — ровно 20. Ради этого функция и существует, вместо записи от руки: естественная запись a + (b - a) * t промахивается мимо b при t = 1 в 41% из 200 000 случайных пар, потому что вычитание теряет младшие биты, а сложение их не возвращает. Здесь считается a * (1 - t) + b * t, который потерять не может.

За пределами [0, 1] экстраполирует, а не отказывает: lerp(10, 20, 2) — это 30. Значение, выехавшее за границы, обычно арифметика вызывающего, а не ошибка, а clamp рядом на тот случай, когда всё-таки ошибка.

Колонка, читающая собственное прошлое

Все функции выше отвечают из ОДНОЙ строки. prev(Колонка, начальное) — исключение: она читает Колонку из предыдущей строки, а на первой отдаёт начальное. Это то, что нужно случайному блужданию — каждая строка есть предыдущая плюс шаг, — и то, чего выражения раньше сказать не могли.

<env count="3600" seed="p001" mode="sequential">
<sequence name="RR">
<gen type="formula" decimals="3"
expr="clamp(prev(RR, 700) + (hash(_count, 3) - 0.5) * 180, 350, 1400)"/>
</sequence>
</env>

prev берёт ИМЯ колонки, невычисленным. Написав RR в любом другом месте того же выражения, вы получите значение ЭТОЙ строки — ровно то, мимо чего prev и смотрит.

Нужен mode="sequential" у <env>, и без него движок скажет об этом. Режим — это обещание, что строка N считается после строки N−1. Без него движок вправе получить любую строку, не трогая предыдущую (так он и выдаёт прогон больше памяти), а значит prev() читал бы строку, которой ещё нет.

mode="sequential" и order="sequential" — разные вещи

order="sequential" у <gen> идёт по значениям генератора в том порядке, в каком они написаны, вместо случайного выбора. Это про значения ОДНОЙ колонки.

mode="sequential" у <env> — про весь ПРОГОН: строки считаются одна за другой. Слово совпадает, предмет — нет.

Чего режим стоит и в чём отказывает:

  • Прогон держится в памяти (движок 1), поколоночные оптимизации выключены. Это честная цена колонки, читающей своё прошлое, и платит её только тот, кто попросил.
  • mode="sequential" вместе с engine="2" или engine="3" — отказ, называющий оба атрибута. Те движки получают любую строку без предыдущей, это их устройство, и оба условия сразу выполнить нельзя.
  • prev() внутри if= тоже отказ. if= — выбор, делаемый построчно, и движок, отвечающий на него, вправе брать строки в любом порядке.

Всё остальное рядом работает: percent=, uniq и distinct не затронуты — колонки регистрируются в порядке объявления, а это тот же порядок, на котором стоит prev().

Списки внутри строки

Последовательность с repeat= кладёт в одно поле несколько значений, склеенных её separator=. Выражение видит склеенный текст — именно он и лежит в поле, — поэтому split и есть тот мост, после которого список становится списком.

<sequence name="Prices">
<gen type="number" value="10..200" repeat="3" separator=","/>
</sequence>
<sequence name="Basket">
<gen if="sum(split(Prices, ',')) > 300" type="text" value="large"/>
<gen type="text" value="ordinary"/>
</sequence>

min и max одинаково читают и список, и россыпь аргументов: работает и max(split(Prices, ',')), и max(1, 9, 4). Пустой разделитель режет на отдельные символы — ту же единицу считает len, — поэтому count(split(s, '')) и len(s) никогда не разойдутся.

at считает с нуля и отказывается от индекса, который индексом не является

at(list, 0) — первый элемент. За концом списка — пустой текст, и это намеренно: repeat="1..4" специально делает строки разной длины, и вопрос про третий элемент строки из двух — настоящий вопрос с пустым ответом. Чтобы спросить заранее, есть count(list).

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

<gen if="at(Prices, 1) > 100" /> <!-- отказ: TDC260 -->
<gen if="at(split(Prices, ','), 1) > 100" /> <!-- что имелось в виду -->

Prices — это склеенный текст, поэтому первая строка просила второй элемент списка из одного и раньше давала пустую колонку, а запуск при этом сообщал об успехе. Написанные прямо ошибки ловит tdcv2 check ещё до первой строки; индекс, который считается на ходу — at(list, _count - 1), — проверяется при сборке строки.

Почему TDC считает трансцендентные функции сам

IEEE-754 задаёт для + - * / и sqrt ровно один допустимый ответ, поэтому о них все языки договариваются. О sin, cos, exp, log и pow он не говорит ничего — каждая libm выбирает свой алгоритм, — и расхождение измеримо, а не умозрительно:

tan(1)
Node3ff8eb245cbee3a6
Python3ff8eb245cbee3a5

Из семидесяти семи взятых значений шестнадцать расходятся хотя бы у двух реализаций из пяти. В timeseries этого не видно: каждое число округляется до десятичной строки, прежде чем стать выводом, — последний бит умирает на выходе. У сравнения шага округления нет, поэтому этот бит превращается в другую строку и другой файл — у инструмента, всё обещание которого в том, что пять реализаций дают одни и те же байты.

Поэтому TDC считает их сам — так же, как он уже сам считает случайные числа, вместо того чтобы доверять каждому языку. Каждая ложится в пределах 4 ulp от истинного значения — там же, где живёт и libm, — и, что куда важнее, на один и тот же double во всех пяти. Совпасть с какой-то конкретной libm — не цель и не могло ею быть: libm'ы не совпадают между собой.

Эта четвёрка проверяется, а не заявляется, — на сетках, которые доходят до краёв области определения. Края и важны: ряд, оборванный на два члена раньше, в середине интервала незаметен, а на краю даёт тринадцать ulp. Ровно эту ошибку проверка и нашла.

У pow граница шире, и причину стоит знать:

показателькак считаетсярасхождение
целый или половинныйвозведение в квадрат, для половины — sqrtрастёт с показателем: ~4 ulp при 3, ~22 при 20
любой другойexp(y · log x)растёт с |y · log x|: ~2 ulp при 1, ~457 при 400

И то и другое — усиление, а не дефект: возведение в квадрат удваивает полученную ошибку, а exp превращает абсолютную ошибку аргумента в относительную ошибку ответа. Двенадцать значащих цифр выживают в обоих случаях.

<sequence name="Month"><gen type="increment" value="1"/></sequence>
<sequence name="Load">
<gen if="cos(Month / 2) > 0.5" type="text" value="peak"/>
<gen if="cos(Month / 2) < -0.5" type="text" value="trough"/>
<gen type="text" value="normal"/>
</sequence>
<sequence name="Tier">
<gen if="pow(2, Month) > 100" type="text" value="large"/>
<gen type="text" value="small"/>
</sequence>
tdcv2 seasonal.tdc
1 peak small
2 peak small
3 normal small
4 normal small
5 trough small
6 trough small
7 trough large
8 trough large

Этот файл прогнали через все пять реализаций, и каждая выдала эти самые байты.

Из того, что функции свои, а не заимствованные, следуют две вещи. pow с целым показателем идёт через возведение в квадрат, поэтому pow(10, 3) — ровно 1000, а не 999.9999999999998; конфиг, сравнивающий с круглым числом, разницу бы заметил. И тригонометрия берёт радианы, градусного варианта нет: одно соглашение, названное один раз.

Пара, которая существует потому, что вычитание теряет

expm1 и log1p — не сокращения для exp(x) - 1 и log(1 + x). Это те же выражения, но посчитанные так, чтобы ответ уцелел:

около нуля определения не возвращают ничего
expm1(1e-20)   1e-20        exp(1e-20) - 1     0
log1p(1e-20)   1e-20        log(1 + 1e-20)     0

Второй столбец — не погрешность округления, это весь ответ целиком. 1 + 1e-20 как double РАВНО единице, поэтому логарифм аргумента вообще не видит; а exp(1e-20) равно 1.0000…, и вычитание съедает все значащие цифры. asinh и atanh построены на log1p по той же причине и наследуют его точность.

hypot спасает от зеркальной беды на другом конце диапазона: sqrt(x² + y²) переполняется в бесконечность при x = 10²⁰⁰, хотя ответ прекрасно представим. А log2 отделяет экспоненту до того, как берёт логарифм, поэтому log2(8) — это 3, а не 2.9999999999999996.

Где граница перестаёт измеряться в ulp

У этих четырёх граница не сводится к «в пределах 4 ulp», и это часть справки, а не сноска:

ФункцияЧто гарантируется
erfв пределах 4 ulp
erfcв пределах 8 ulp — она проходит через e^(−x²), а эта экспонента несёт округление квадрата
gammaточно на целых до 23, в пределах 7 ulp на всех 171, что влезают в double; двенадцать значащих цифр в остальном
lgammaв пределах 32 ulp вдали от нулей; при x = 1 и x = 2 осмысленная граница абсолютная, меньше 10⁻¹³

Интересен здесь lgamma. Он равен нулю в 1 и в 2, и никакой метод, складывающий слагаемые порядка единицы, не может быть относительно точен в том, что они сократились в ноль — заявление там обязано быть абсолютным, и оно такое. Оба нуля выходят ровно нулями.

gamma вне целых заканчивается экспонентой, поэтому расхождение растёт вместе с log Γ(x) — то же усиление, что и у pow, по той же причине. Ради этого целое число и идёт факториальным путём.

Чего нет намеренно

Математика, которой генератору данных нечего делать. besselj, bessely, airy, elliptic_k, elliptic_e и polygamma отвергаются по имени, а не угадываются:

tdcv2 check seasonal.tdc
error[TDC257]: besselj() is not available yet in an if expression

Обратите внимание, чего сообщение НЕ говорит: «может, вы имели в виду beta?» Расстояние редактирования предложило бы именно это, а exp — совсем не функция ошибок. Имя из этого списка получает в ответ причину, а не догадку.

Каждая из них — проект, а не функция, и ни одна никогда всерьёз не относилась к предикату строки. Они остаются в списке, чтобы человек, потянувшийся за одной из них, получил ответ, а не «неизвестная функция».

Циклы и рекурсия. Движок выбирается по конфигу до генерации первой строки, preflight() оценивает память до прогона, а --jobs режет строки между воркерами. Всем троим нужно знать работу на строку, не выполняя её. Цикл ломает все три сразу, а то, ради чего к циклу тянутся — «чётная ли это строка?», — это %.

Побитовые операторы. _count & 1 — это _count % 2, записанное для машины. Они разбираются, чтобы сообщение могло назвать их по имени, и затем отвергаются.

Когда выражения не хватает

У последовательности <compute> есть целочисленное деление, остатки, работа со строками, кодировки и контрольные цифры. Она даёт значение, как любая другая последовательность, и if= затем сравнивает уже его:

<sequence name="Checksum">
<compute><result><mod><to_number><field name="Account"/></to_number><int v="97"/></mod></result></compute>
</sequence>
<sequence name="Flag">
<gen if="Checksum == 0" type="text" value="divisible"/>
<gen type="text" value="."/>
</sequence>

Конфиг похож на XML, но это не XML

TDC не разворачивает сущности, поэтому &lt; — это четыре обычных символа, а не <. Пишите символ напрямую:

<gen if="Weight > 20" .../> <!-- да -->
<gen if="Weight &gt; 20" .../> <!-- нет: TDC103, сущность — это четыре символа -->