it adds. it subtracts. it uses zero arithmetic operators to do it.
no +, no -, no *, no /, no ++, no --, no +=, no -=. anywhere in
the js. and it still gets the right answers, which is the whole point.
it's actual peano arithmetic too, not a trick where i hid the + somewhere.
addition really is "count up one at a time", so that's what it does.
demo: https://macpanthor.github.io/peano-engine/ · built by macpanthor
MIT licensed. fork it, steal it, do whatever.
the array-as-counter thing isn't specific to calculators. any time you need to
count without a + — code golf, interview puzzles, teaching recursion, weird
contest rules, "no arithmetic operators" homework — this works:
var n = []; // n.length is your number
n.push(null); // n.length just went up by 1
n.pop(); // n.length just went down by 1that's it. that's the whole trick. .length is data, push/pop are your
increment and decrement, and you never type an operator.
once you have that, the successor table falls out of it, and addition is just a
loop. same idea works in any language with growable arrays — python list,
ruby Array, php arrays, even a Vec in rust.
fork it and use it in something dumber than a calculator. i'd genuinely like to see that.
this is the same trick, just applied to a whole calculator.
count an array up to 100000 and write down every step, and you've got a successor table. do it backwards by popping and you've got a predecessor table.
NEXT[0] = 1
NEXT[1] = 2
NEXT[99999] = 100000after that a + b is just "start at a, ask NEXT for directions b times":
result = a
repeat b times:
result = NEXT[result]
subtraction is the exact same loop through PREV.
so yeah. two 100000-entry tables and a loop. that's the calculator.
because it's actually doing the work. 50000 + 50000 is 50000 separate lookups
and takes about two seconds. the progress bar isn't fake — it's counting.
it does 200 lookups per animation frame. tuned that number by vibe. small sums are instant, big ones are funny.
one number in the successor table is wrong:
NEXT[41] = 43;it only does anything if a walk lands on 41 and then tries to leave. so
0 + 41 is fine but 0 + 42 gives you 43. cross that point and everything
after it is 1 too high.
been there since v1.0. not fixing it.
there's a <!-- can you find the bug? --> at the top of the html if anyone
goes looking.
| numbers | 0 to 100000 |
| too big | says OVERFLOW and gives up |
| too small | stops at 0, doesn't do negatives |
| bad input | rejects it |
| out of range | clamps it back into 0-100000 |
index.html the page
assets/
styles.css all the styling
script.js the engine (zero arithmetic operators)
logo.webp, logo.png
favicon.ico, favicon-16x16.png, favicon-32x32.png
apple-touch-icon.png
android-chrome-192x192.png, android-chrome-512x512.png
three real files. no build step, no npm, no dependencies. not even a webfont.
just open index.html.
git clone https://github.com/macpanthor/peano-engine.git
cd peano-engineopen index.html. done.
or if you'd rather serve it:
python -m http.server 8000the whole thing is a lie if there's a + hiding in there, so don't take my word
for it.
node — strips comments then counts operator matches:
node -e "const s=require('fs').readFileSync('assets/script.js','utf8');console.log(s.split('\n').filter(l=>{const c=l.split('//')[0];return c.trim()&&/[+*]|\+\+|--|\+=|-=|\//.test(c)}).length)"gives you 0.
powershell (windows):
Get-Content assets\script.js |
ForEach-Object { $_.Split('//')[0] } |
Select-String -Pattern '[+*]|\+\+|--|\+=|-=|/'prints nothing.
bash / mac / linux:
sed 's|//.*||' assets/script.js | grep -nE '[+*]|\+\+|--|\+=|-=|/'also prints nothing.
heads up: plain old grep '[+*]' assets/script.js will match a bunch of
lines. that's fine. my comments talk about operators constantly. the claim is
about the code, not the comments.
- counters are array lengths everywhere. that's why it's
ticks.push(null)thencount = ticks.lengthinstead ofcount++. - progress % is precomputed, not divided. at the start of a run it deals the hop budget into 100 buckets and then just counts full buckets. first version divided every frame and made a 100k run take 60 seconds instead of 3.
- tables get built on page load, not fetched. 100k successors plus 100k predecessors.
PREV[0]is undefined on purpose. nothing comes before nothing.
did try to do this properly:
- tabs are real tabs (
role="tablist"/tab/tabpanel) with arrow keys - the +/- thing is a
radiogroupwitharia-checked - the answer area is
aria-liveso it gets announced - focus rings everywhere, progress bar is a
role="progressbar" - respects
prefers-reduced-motionandforced-colors
MIT — do whatever you like with it.
written at an unreasonable hour. be nice to it.