Show HN: Wyrm – Solve algebra by touch, built on an open-source soundness engine

Hacker News Top Tools

Summary

Wyrm is an open-source symbolic algebra engine in TypeScript that powers a gesture-based algebra app for iOS and Android. It guarantees soundness by construction through conditional rewrite rules, allowing users to solve equations by touch.

There is a mobile game called DragonBox. It sort of tricks you into learning algebra by starting with very abstract manipulations of a puzzle that must follow rules... gradually the game teaches you more and more rules and also strips out the more abstract elements until on the last levels you are finally solving real equations. I loved it, it taught my kids algebra.... and it was just fun.<p>Over the years I often thought that there should be a calculator for Algebra that works this way... something where you can drag terms around and cancel &amp; distribute with gestures, but most importantly enter your own problems. It should also do more kinds of problems than DragonBox allowed. So I finally decided to build it.<p><a href="https:&#x2F;&#x2F;dicroce.github.io&#x2F;wyrm&#x2F;home.html" rel="nofollow">https:&#x2F;&#x2F;dicroce.github.io&#x2F;wyrm&#x2F;home.html</a><p>Here&#x27;s a video showing it: <a href="https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=_STbS4zvIlU" rel="nofollow">https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=_STbS4zvIlU</a>. If you&#x27;d rather just play with it: there&#x27;s a limited in-browser demo (real engine, a few example equations, no download) on the landing page — <a href="https:&#x2F;&#x2F;dicroce.github.io&#x2F;wyrm&#x2F;home.html" rel="nofollow">https:&#x2F;&#x2F;dicroce.github.io&#x2F;wyrm&#x2F;home.html</a>.<p>The app can be found on iOS (<a href="https:&#x2F;&#x2F;apps.apple.com&#x2F;us&#x2F;app&#x2F;wyrm-math&#x2F;id6782342042">https:&#x2F;&#x2F;apps.apple.com&#x2F;us&#x2F;app&#x2F;wyrm-math&#x2F;id6782342042</a>) and as of this week on Google Play (<a href="https:&#x2F;&#x2F;play.google.com&#x2F;store&#x2F;apps&#x2F;details?id=com.dicroce.wyrm">https:&#x2F;&#x2F;play.google.com&#x2F;store&#x2F;apps&#x2F;details?id=com.dicroce.wy...</a>).<p>I also decided to open source the underlying math engine so others could build on it: <a href="https:&#x2F;&#x2F;github.com&#x2F;dicroce&#x2F;wyrm_math" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;dicroce&#x2F;wyrm_math</a>. My goal for the engine btw is to build it all the way up to Calculus.<p>Monetization is deliberately boring: the engine is free (MIT), and the polished gesture app is $4.99 once. No subscriptions, ads, accounts, or analytics.<p>I&#x27;d love feedback on the engine design — especially from anyone who&#x27;s worked on CAS or proof-assistant-adjacent problems. And if you played DragonBox as a kid and wished it went further: this is for you!
Original Article
View Cached Full Text

Cached at: 07/10/26, 09:13 PM

dicroce/wyrm_math

Source: https://github.com/dicroce/wyrm_math

wyrm-math

An exact, conditionally-sound symbolic algebra engine for building manipulative math interfaces — the kind where users solve equations by dragging terms across the equals sign, tapping a power to expand it, or pulling a shared factor out of two terms.

The core invariant: legal moves are possible, illegal moves are impossible. Equations are never validated — they are only ever transformed by rewrite rules, so every reachable state is sound by construction. And soundness is conditional: moves that are only valid under a condition (dividing by b requires b ≠ 0) or that can introduce extraneous solutions (multiplying both sides, squaring) are not forbidden — their conditions become first-class, visible Assumptions that travel with the equation.

Pure TypeScript, zero dependencies, zero DOM — runs in Node, browsers, workers, native webviews, anywhere.

wyrm-math is the engine behind Wyrm Math, a gesture-based algebra app for iOS and Android — try the in-browser demo or get the app. The engine is MIT; the app is how the project sustains itself.

import {
  parseEquation, Derivation,
  enumerateMoves, ruleById, layoutNode, exprToString,
} from "wyrm-math";

const d = new Derivation(parseEquation("2x + 3 = 11"));

// What can the user legally do right now?
const moves = enumerateMoves(d.current);

// Drag the 3 across the equals sign (the UI picks a Move; the engine
// guarantees it is legal — enumeration is precondition-checked):
const move = moves.find((m) => m.ruleId === "move-term-across")!;
d.apply(ruleById(move.ruleId), move.location, move.params);

console.log(exprToString(d.current.equation)); // 2x = 11 + -3

// Render it however you like: layoutNode gives positioned, id-keyed boxes
// and glyphs from static metric tables (no font measurement needed).
const layout = layoutNode(d.current.equation);

What’s inside

The public API is src/index.ts, organized into ten documented groups — it reads as a table of contents:

GroupWhat it gives you
Expression treesImmutable AST with stable node ids. N-ary Sum/Product; no subtraction or division nodes (a − b is Sum(a, Neg(b)); division is a Fraction with numerator/denominator lists). Smart constructors maintain the structural invariants.
Exact arithmeticRational over bigint. No floating point anywhere — √2 is an undefined point, not 1.4142.
EvaluationtruthValue(equation, env) decides any relation (= < ≤ > ≥) at a sample point, exactly, or returns undefined where a side is undefined.
Parsing & printingparseEquation("2x + 3 = 11")exprToString — round-trip property-tested. Implicit multiplication, fractions, powers, radicals; decimals rejected (the engine is exact).
Judgments & assumptionsThe unit of state is { assumptions, equation }. Restrictions (moves that may LOSE solutions: b ≠ 0), Extensions (moves that may GAIN them: carry the original equation as an obligation, settled by checkSolution), Pinned (user what-ifs). Discharged assumptions are recorded, never deleted.
Rules & derivationsRule.apply is the only way an equation changes. The derivation log is an append-only tree: undo moves a pointer, abandoned branches stay live, case splits and disjunctions fork into live siblings.
Built-in rules~25 rules covering linear equations, like terms, distribution, fractions, exponent laws, inequalities (sign-aware, relation-flipping), and quadratics (x² = 9 branches to x = ±3; zero-product). Every rule ships with a property test that it respects the solution set under its assumptions.
Move enumerationenumerateMoves(judgment) returns every legal affordance with gesture anchors (handle, dropTarget). Sound for all rules, complete for the finite ones. Pin x = 0 and every divide-by-x affordance disappears automatically.
Layout geometrylayoutNode maps trees to positioned, id-keyed boxes and glyphs (fraction stacking, superscripts, radicals) from static metric tables. hitTest is a geometry query. Subtree geometry is context-independent up to translation+scale — which is what makes id-keyed animation possible.
Rule-authoring toolkitId-preserving rebuilds, the invariant-repairing splice, diff bookkeeping, and assumption-lifecycle queries for writing new rules.

ARCHITECTURE.md explains the invariants and contracts in depth.

Design commitments

  • Exactness. All arithmetic is bigint rationals. Points where an expression is undefined (division by zero, irrational roots) are treated as undefined, never approximated. The engine-wide soundness contract is truth-where-both-defined.
  • Stable ids. Every node has an id; operations preserve the ids of untouched subtrees. This is the currency of hit testing and animation: a renderer can match nodes across a rewrite and move them rigidly.
  • Conditional soundness. For ordinary and Restriction-emitting rules, property tests rejection-sample substitutions to those satisfying the result judgment’s assumptions and assert truth preservation. For Extension-emitting rules the check weakens to one direction (solutions are never lost), with checkSolution covering the gain obligation.
  • Disjunction. Branching rules return several outcomes whose solution sets union to the original’s (x² = 9x = 3 or x = −3); the derivation tree holds all arms as live, navigable states.

Development

pnpm install
pnpm test        # vitest + fast-check (property tests are the soul of this project)
pnpm typecheck
pnpm build       # emits dist/ (ESM + d.ts)

The engine must stay DOM-free: tsconfig.json has no DOM lib and test/boundary.test.ts scans the sources for browser globals.

License

MIT

Similar Articles

Show HN: I Built LangGraph for Swift

Hacker News Top

Swarm is a Swift framework for building agentic workflows and multi-agent systems, with type-safe tool calling, durable checkpointing, and support for multiple LLM providers.

Scheme is a Hoot

Lobsters Hottest

The author shares their experience learning Scheme and using Hoot to compile it to WebAssembly, encountering stability issues but successfully running physics simulations in the browser.

Show HN: Inkwash, a watercolor sketching app and explanation

Hacker News Top

Inkwash is a WebGL2-based watercolor sketching app that simulates pigment flow and paper interaction, generated with Claude Fable 5. The article explains the technical pipeline of floating-point textures and shaders behind the realistic watercolor effect.