Выражения
Маленький язык внутри 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>
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, отвечает так:
9007199254740993 == 9007199254740992 true 9007199254740993 - 9007199254740992 0
И то и другое неверно, и неверно молча — а для генератора данных это худший вид ошибки: прогон завершается, файл выглядит нормально. Поэтому операнд, который является целым, и несётся как целое, а double становится только когда его об этом попросят:
| литерал без точки и экспоненты | остаётся целым |
| колонка, значение которой читается как цифры | сравнивается как целое с другим целым |
+ - * % над двумя целыми | остаются целыми |
/ | всегда с плавающей точкой — деление не замкнуто на целых |
всё, что уходит в sqrt, log, sin… | становится double: у них точного ответа нет |
Домен — знаковые 64 бита, тот же, что у слоя compute: это самое широкое целое, которое все пять реализаций держат сами. За его пределом — отказ, теми же словами, что у compute, а не тихое сползание обратно в плавающую точку:
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) | 2 | i-й элемент, счёт с нуля |
sum(list) | 1 | сумма — остаётся целой, пока целы все элементы |
mean(list) median(list) | 1 | среднее и середина |
stddev(list) | 1 | популяционное стандартное отклонение, делится на n |
sqrt(x) | 1 | квадратный корень |
pow(x, y) | 2 | x в степени y |
exp(x) | 1 | e в степени 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) | 1 | eˣ−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) | |
|---|---|
| Node | 3ff8eb245cbee3a6 |
| Python | 3ff8eb245cbee3a5 |
Из семидесяти семи взятых значений шестнадцать расходятся хотя бы у двух реализаций из пяти.
В 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>
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 отвергаются по имени, а не угадываются:
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 не разворачивает сущности, поэтому < — это четыре обычных символа, а не <. Пишите
символ напрямую:
<gen if="Weight > 20" .../> <!-- да -->
<gen if="Weight > 20" .../> <!-- нет: TDC100, и сообщение объяснит почему -->