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

Выражения

Маленький язык внутри if= — и внутри filter= у пула, который читается так же. Он решает, участвует ли <gen>, <line>, <case> или <data> в строке.

<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 становится только когда его об этом попросят:

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

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

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

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

Операторы

ГруппаОператоры
сравнение== != === !== < > <= >=
логика&& || !
арифметика+ - * / %
принадлежностьin
выборa ? b : c
% евклидов, и это не то, что делает ваш язык

-3 % 2 здесь 1. JavaScript, Java, C# и Rust отвечают −1; Python отвечает 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длина вектора, без переполнения по дороге
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между двумя способами записать угол

Всё, что над чертой, — точное: собрано из сравнений и той арифметики, которую 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: строковая функция читает аргумент как текст, а не как число.

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

Последовательность с 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" .../> <!-- нет: TDC100, и сообщение объяснит почему -->