the published fragment. --> The Composable Calculator

Dynamic operations for a calculator

Operations come and go.
The answers stay right.

Every arithmetic operation is a plugin. Adding one more plugin adds another operation to the calculator; taking one out should make it forget. Toggle the operations below and type an expression — both runtimes calculate the same expression, and only one of them does it accurately.

The lab

Toggle an operation. Then ask both calculators what they can do.

Five primitives and one derived operation. % is a remainder, and a remainder is defined as a - floor(a / b) * b — so it works only for as long as subtraction, multiplication and division all do. Turn off division and watch what each calculator goes on offering.

Both sides below are an interactive model of examples/run_calculator.py, which runs the same comparison against the real runtime and prints the same numbers.

Walkthrough · not started Press Next step to begin
CORDISeffects on a fiber
help — what it offers
operations
last evaluation
CONVENTIONAL REGISTRYsetup() / teardown()
help — what it offers
operations
last evaluation
WALKTHROUGH
Free play

Drive the two calculators yourself. The walkthrough above never touches these controls, so anything you change here is your own experiment - press Restart to put the walkthrough back in charge.

operation plugins
expression
a plugin that fails mid-load
 

Why it diverges

The teardown is complete. It is still not enough.

Not a straw man

The conventional teardown has no bug in it

When division is unregistered, its teardown() removes it from all four of the calculator's tables — the operator table, the precedence table, the tokenizer's alphabet and the catalog help reads. There is no forgotten line.

What it cannot do is tell % that the division it resolved at registration has gone away. require resolved once and handed back a direct reference; nothing invalidates it later, because nothing is tracking it. So the remainder stays registered, stays advertised, and fails the next time it is asked to divide.

Declarations

One line of dependency management

On the cordis side the remainder declares inject=["op.sub", "op.mul", "op.div"]. That is the whole of it. Retiring division changes the fiber's target, the runtime deactivates the remainder, and its catalog entry goes with it — so the calculator stops offering a remainder the moment it stops being able to compute one.

The memo cache is the sting. It is opened while evaluating an expression, long after setup returned, so a hand-written teardown cannot reach it — and on the conventional side it survives to answer, correctly, for exactly the pairs it happens to hold.

Run it for real

This page models it; these commands measure it

The walkthrough is modelled on tests/UAT/calculator.sh, which runs the same sequence against the real runtime and asserts each step.

uv run python examples/run_calculator.py --scenario remove
uv run python examples/run_calculator.py --scenario all
./tests/UAT/calculator.sh --auto # the same run, asserted step by step

Its companion page, docs/demo.html, makes the same argument by counting what a retired component leaves behind in a live process. (Named rather than linked: the two pages sit beside each other in a clone, but are published at separate addresses.)

cordispy is a Python realization of the runtime described in A Programming Paradigm for Spatiotemporal Composability (Shi, Zhang, Cui). Both calculators here run the same arithmetic out of the same shared engine (examples/calc/engine.py); only composition differs. The conventional side is a faithful implementation with no planted bug — see examples/calc/naive_side.py, whose header says exactly which two properties of the design every difference follows from.