Вышел движок wyrm-math — математический движок для интерфейсов, где уравнения решают жестами: перетаскивают слагаемые через знак равенства, раскрывают скобки, выносят общий множитель. Написан на чистом TypeScript, без зависимостей и DOM — работает в Node, браузере, веб-воркерах, нативных webview.
Главная фишка: conditionally-sound. Уравнения никогда не проверяются на валидность — они только преобразуются rewrite-правилами. Любое достижимое состояние корректно по построению. Операции, которые требуют условий (деление на b требует b ≠ 0) или могут добавить посторонние корни (возведение в квадрат), не запрещены — их условия становятся явными Assumptions, которые путешествуют с уравнением.
wyrm-math — это движок внутри приложения Wyrm Math, жестовой алгебры для iOS и Android. Код движка под лицензией MIT, приложение — способ проекта существовать.
API организован в десять групп. Expression trees — иммутабельное AST со стабильными id узлов. Там нет узлов вычитания и деления: a - b это Sum(a, Neg(b)), а деление — Fraction со списками числителя и знаменателя. Умные конструкторы следят за инвариантами. Exact arithmetic — рациональные числа на bigint, никаких float, √2 это undefined point, а не 1.4142. Функция truthValue точно решает отношение (=, <, >, ≤, ≥) в точке или возвращает undefined, если сторона не определена. parseEquation("2x + 3 = 11") и exprToString обратны друг другу — проверено property tests.
Состояние — это пара { assumptions, equation }. Есть три типа assumptions: Restrictions (могут потерять решения), Extensions (могут добавить — несут исходное уравнение как обязательство, которое проверяет checkSolution), Pinned (пользовательские «what if»). Выполненные assumptions записываются, но не удаляются. Единственный способ изменить уравнение — Rule.apply. Лог изменений — дерево с append-only: отмена двигает указатель, заброшенные ветки живут, case splits форкаются. Около 25 встроенных правил: линейные уравнения, подобные, распределение, дроби, степени, неравенства (с учётом знака), квадратные. Каждое правило проверяется property test на сохранение множества решений.
enumerateMoves возвращает все легальные действия с gesture anchors. Закрепи x = 0 — и все действия с делением на x пропадут автоматически. layoutNode превращает дерево в позиционированные блоки с id и glyphs (дроби, степени, радикалы) из статических метрик — никаких замеров шрифтов. hitTest — геометрический запрос. Поддеревья контекстно-независимы до translation+scale, что позволяет анимировать по id. Сборка — pnpm build, тесты — vitest + fast-check. Движок DOM-free — в конфиге нет DOM lib, и тест boundary.test.ts проверяет, что нет браузерных глобал.