Appearance
Decimal places
Examples on this site use -- annotations for readability
The rule language has no comment syntax. Strip anything from -- onward before pasting an expression into the editor.
Almost every real rule in this system is about a technique at a decimal place, not about a specific number. "Add 8 when the affected digit is 2" and "add 80 when the tens digit is 2" are the same technique performed one place apart. Writing those as separate rules would multiply your work for no benefit.
So the language derives three variables for you, describing where the candidate acts:
| Variable | Meaning | For current_value = -70, prev_sum = 325 |
|---|---|---|
place | The decimal place affected: 1, 10, 100, … | 10 |
formula | The normalized 1–9 digit, ignoring sign and place | 7 |
digitBefore | The digit of prev_sum at that same place, before the step | 2 |
digitBefore is the important one. It tells you what the abacus looks like at the affected place right now, which is what decides whether a technique is even possible.
The standard shape of a pattern rule
Nearly every canonical rule follows one three-clause template:
current_value > 0 AND formula = 8 AND digitBefore IN (0, 1)Read left to right:
current_value > 0— the operation is an addition. Usecurrent_value < 0for subtraction.formula = 8— the technique is an 8.digitBefore IN (0, 1)— the affected digit is currently 0 or 1.
Because formula and digitBefore are place-relative, that one rule matches 2 + 8, 12 + 8, 120 + 80, and 3100 + 800 alike. See the rule catalog for the full set built this way.
Atomic and composite values
These three variables only make sense when the candidate touches exactly one decimal place. Such a value is called atomic:
| Value | Atomic? | Why |
|---|---|---|
+7 | yes | one non-zero digit, at the ones place |
-40 | yes | one non-zero digit, at the tens place |
+300 | yes | one non-zero digit, at the hundreds place |
+47 | no | two non-zero digits: 4 at tens and 7 at ones |
-89 | no | two non-zero digits |
For a composite value like +47 there is no single place, no single formula digit, and no single "digit before" — so place, formula, and digitBefore are all null.
Composite values are not hypothetical: a recipe can be configured to produce them deliberately, so a step like +47 is a real thing your rule may be asked about. See multi-place combo steps.
The null trap
This is the most common source of a rule that looks right but behaves strangely.
When these variables are null, comparisons still evaluate — they just do not do what you would expect. Positive tests fail, and negative tests pass.
Given a composite candidate like +47:
| Expression | Result | Effect on the step |
|---|---|---|
formula = 8 | fails | rejects it |
formula >= 1 | fails | rejects it |
place IN (1, 10) | fails | rejects it |
digitBefore = 2 | fails | rejects it |
formula != 8 | passes | allows it |
formula < 1 | passes | allows it |
place NOT IN (1, 10) | passes | allows it |
digitBefore != 2 | passes | allows it |
Two consequences worth internalizing:
A rule meant to exclude something can stop excluding it. formula != 9, intended as "never a 9", quietly permits every composite value — null is not 9, so the test passes.
A rule meant to restrict place can reject everything. place = 10, intended as "tens only", quietly rejects every composite value, even one whose tens digit is exactly what you wanted.
Neither is a bug. A composite value genuinely has no single formula or place, and the comparison is answering honestly. Both are just easy to write by accident.
Rule of thumb
If a rule uses place, formula, or digitBefore, it is a rule about atomic steps. That is usually exactly what you want. Reach for digitAt below only when you deliberately need to inspect one place of a value that may touch several.
Inspecting a specific place with digitAt
digitAt(value, place) reads the digit at any place you name, and does not care whether the value is atomic. It is the composite-safe way to talk about places:
digitAt(current_value, 10) = 4 -- the tens digit of the candidate is 4
digitAt(current_value, 1) != 9 -- the ones digit of the candidate is not 9
digitAt(prev_sum, 100) IN (0, 5) -- hundreds digit of the running total is 0 or 5For current_value = 47, digitAt(current_value, 10) is 4 and digitAt(current_value, 1) is 7 — while place, formula, and digitBefore are all null.
A common use is pinning the opening position, where digitBefore would not help because it only ever describes the place the candidate itself affects:
IF step_index = 0 THEN digitAt(prev_sum, 1) = 0 -- start from a clean ones digitChoosing between them
| You want to say | Use |
|---|---|
| "This technique, at whatever place it occurs" | formula + digitBefore |
| "Only steps in the ones place" | place = 1 |
| "This digit of a value, whatever else the value touches" | digitAt(x, place) |
| "Never a 9 anywhere, including in composite steps" | digitAt(current_value, 1) != 9 and so on per place, not formula != 9 |
A rule scoped to one place-channel of a recipe may not reference current_sum.
Inside a single channel, "the sum after this step" is not a state the exercise ever actually passes through, so it is rejected when the recipe is compiled. Put whole-step sum conditions in an ordinary, unscoped constraint rule instead. See multi-place combo steps.
See also
- Context variables — exact definitions and the full null contract
- Built-in functions —
digitAtand the digit helpers - Canonical rules — every seeded rule, all built from this template
- Restricting places — applying this at the recipe level