<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P007-calculator/ | updated: 2026-09-29 -->

# P007 Calculator

Type an expression with + - * / and parentheses; it is turned into postfix and worked out on a stack in exact fractions, so 0.1 + 0.2 is 0.3. Clear error messages, a history and ans for the last answer.

EML-P project `projects/calculator` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/calculator` runs it and `eml project verify projects/calculator` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: rpn-evaluator (https://efficientnewlanguage.org/cases/093-rpn-evaluator/), simple-stack (https://efficientnewlanguage.org/cases/042-simple-stack/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
ans + 1

2 +
(1 + 2
1 + 2)
3 4
4 / (2 - 2)
2 * x
1..2
.5 + 1
*3
7 / 0
()
2 * (3 + 4)
quit
```

Screen:

```text
Calculator: + - * / and ( ), in exact fractions; ans is the last answer.
Commands: history, clear, quit.
calc> ans + 1
There is no answer yet for ans.
calc> 
Type an expression, or quit.
calc> 2 +
The expression is incomplete.
calc> (1 + 2
A ( is not closed.
calc> 1 + 2)
There is no ( for this ).
calc> 3 4
An operator is missing before 4.
calc> 4 / (2 - 2)
Division by zero.
calc> 2 * x
Unexpected character: x
calc> 1..2
Not a number: 1..2
calc> .5 + 1
Not a number: .5
calc> *3
A number is missing before *.
calc> 7 / 0
Division by zero.
calc> ()
A number is missing before ).
calc> 2 * (3 + 4)
= 14
calc> quit
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
2 * (3 + 4)
0.1 + 0.2
10 / 4
1 / 3
ans * 3
2 / 3
-(2 + 3) * 4
2 * -3
1 - 2 / 3
123456789 * 987654321
ans / 987654321
7 - 2 - 1
history
clear
history
quit
```

Screen:

```text
Calculator: + - * / and ( ), in exact fractions; ans is the last answer.
Commands: history, clear, quit.
calc> 2 * (3 + 4)
= 14
calc> 0.1 + 0.2
= 0.3
calc> 10 / 4
= 2.5
calc> 1 / 3
= 1/3, about 0.3333333333
calc> ans * 3
= 1
calc> 2 / 3
= 2/3, about 0.6666666667
calc> -(2 + 3) * 4
= -20
calc> 2 * -3
= -6
calc> 1 - 2 / 3
= 1/3, about 0.3333333333
calc> 123456789 * 987654321
= 121932631112635269
calc> ans / 987654321
= 123456789
calc> 7 - 2 - 1
= 4
calc> history
  1) 2 * (3 + 4) = 14
  2) 0.1 + 0.2 = 0.3
  3) 10 / 4 = 2.5
  4) 1 / 3 = 1/3, about 0.3333333333
  5) ans * 3 = 1
  6) 2 / 3 = 2/3, about 0.6666666667
  7) -(2 + 3) * 4 = -20
  8) 2 * -3 = -6
  9) 1 - 2 / 3 = 1/3, about 0.3333333333
  10) 123456789 * 987654321 = 121932631112635269
  11) ans / 987654321 = 123456789
  12) 7 - 2 - 1 = 4
calc> clear
History and ans cleared.
calc> history
  (nothing yet)
calc> quit
Bye.
```

## Modules

### main.eml

```eml
# P007 calculator: type an expression and get its value - exactly, because
# every number is a fraction. ans is the last answer; history lists what was
# worked out, and clear forgets it. The history lives while the program runs.
import expr
import frac

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

"Calculator: + - * / and ( ), in exact fractions; ans is the last answer." ^0
"Commands: history, clear, quit." ^0
[] => history
None => ans
True => running
while running:
    trim(input("calc> ")) => line
    if line == "quit":
        False => running
    elif line == "history":
        if len(history) == 0:
            "  (nothing yet)" ^0
        0 => i
        for h in history:
            i + 1 => i
            ("  " + str(i) + ") " + h[0] + " = " + frac.shown(h[1])) ^0
    elif line == "clear":
        [] => history
        None => ans
        "History and ans cleared." ^0
    elif line == "":
        "Type an expression, or quit." ^0
    else:
        expr.run(line, ans) => r
        if r[0]:
            ("= " + frac.shown(r[1])) ^0
            history + [[line, r[1]]] => history
            r[1] => ans
        else:
            r[1] ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import expr
import frac

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

print("Calculator: + - * / and ( ), in exact fractions; ans is the last answer.")
print("Commands: history, clear, quit.")
history = []
ans = None
running = True
while running:
    line = trim(input("calc> "))
    if line == "quit":
        running = False
    elif line == "history":
        if len(history) == 0:
            print("  (nothing yet)")
        i = 0
        for h in history:
            i = i + 1
            print("  " + str(i) + ") " + h[0] + " = " + frac.shown(h[1]))
    elif line == "clear":
        history = []
        ans = None
        print("History and ans cleared.")
    elif line == "":
        print("Type an expression, or quit.")
    else:
        r = expr.run(line, ans)
        if r[0]:
            print("= " + frac.shown(r[1]))
            history = history + [[line, r[1]]]
            ans = r[1]
        else:
            print(r[1])
print("Bye.")
```

### expr.eml

```eml
# P007 calculator - from typed text to an answer, in three steps. tokens()
# cuts the text into numbers, operators and parentheses; postfix() puts them
# in postfix order with the shunting-yard method, checking the shape of the
# expression as it goes; evaluate() works the postfix out on a stack. Each
# step returns [True, result] or [False, what is wrong].
import frac

def tokens(s):
    # [kind, value, text]: kind is "num" (value a fraction), "op" (+ - * /),
    # "(", ")" or "ans".
    [] => out
    0 => i
    len(s) => n
    while i < n:
        s[i] => c
        if c == " ":
            i + 1 => i
        elif c in "0123456789.":
            i => start
            0 => points
            while i < n and s[i] in "0123456789.":
                if s[i] == ".":
                    points + 1 => points
                i + 1 => i
            s[start:i] => text
            if points > 1 or text[0] == "." or text[len(text) - 1] == ".":
                return [False, "Not a number: " + text]
            out + [["num", frac.decimal(text), text]] => out
        elif c in "+-*/":
            out + [["op", c, c]] => out
            i + 1 => i
        elif c == "(" or c == ")":
            out + [[c, c, c]] => out
            i + 1 => i
        elif s[i:i + 3] == "ans":
            out + [["ans", "ans", "ans"]] => out
            i + 3 => i
        else:
            return [False, "Unexpected character: " + c]
    return [True, out]

def rank(t):
    # How tightly an operator binds: minus-in-front ("neg") above * and /,
    # above + and -.
    if t[0] == "neg":
        return 3
    if t[1] == "*" or t[1] == "/":
        return 2
    return 1

def postfix(toks):
    [] => out
    [] => ops
    True => want_number
    for t in toks:
        if want_number:
            if t[0] == "num" or t[0] == "ans":
                out + [t] => out
                False => want_number
            elif t[0] == "(":
                ops + [t] => ops
            elif t[0] == "op" and t[1] == "-":
                ops + [["neg", "-", "-"]] => ops
            else:
                return [False, "A number is missing before " + t[2] + "."]
        elif t[0] == "op":
            # Left to right: first finish every operator on the stack that
            # binds at least as tightly.
            while len(ops) > 0 and ops[len(ops) - 1][0] != "(" and rank(ops[len(ops) - 1]) >= rank(t):
                out + [ops[len(ops) - 1]] => out
                ops[0:len(ops) - 1] => ops
            ops + [t] => ops
            True => want_number
        elif t[0] == ")":
            while len(ops) > 0 and ops[len(ops) - 1][0] != "(":
                out + [ops[len(ops) - 1]] => out
                ops[0:len(ops) - 1] => ops
            if len(ops) == 0:
                return [False, "There is no ( for this )."]
            ops[0:len(ops) - 1] => ops
        else:
            return [False, "An operator is missing before " + t[2] + "."]
    if want_number:
        return [False, "The expression is incomplete."]
    while len(ops) > 0:
        if ops[len(ops) - 1][0] == "(":
            return [False, "A ( is not closed."]
        out + [ops[len(ops) - 1]] => out
        ops[0:len(ops) - 1] => ops
    return [True, out]

def evaluate(post, ans):
    # Work the postfix out on a stack; ans is the last answer or None.
    [] => stack
    for t in post:
        if t[0] == "num":
            stack + [t[1]] => stack
        elif t[0] == "ans":
            if ans == None:
                return [False, "There is no answer yet for ans."]
            stack + [ans] => stack
        elif t[0] == "neg":
            stack[0:len(stack) - 1] + [frac.negated(stack[len(stack) - 1])] => stack
        else:
            stack[len(stack) - 2] => x
            stack[len(stack) - 1] => y
            stack[0:len(stack) - 2] => stack
            if t[1] == "+":
                frac.plus(x, y) => z
            elif t[1] == "-":
                frac.minus(x, y) => z
            elif t[1] == "*":
                frac.times(x, y) => z
            else:
                if y[0] == 0:
                    return [False, "Division by zero."]
                frac.over(x, y) => z
            stack + [z] => stack
    return [True, stack[0]]

def run(text, ans):
    tokens(text) => r
    if r[0]:
        postfix(r[1]) => r
    if r[0]:
        evaluate(r[1], ans) => r
    return r
```

Python projection of expr.eml:

```python
import frac

def tokens(s):
    out = []
    i = 0
    n = len(s)
    while i < n:
        c = s[i]
        if c == " ":
            i = i + 1
        elif c in "0123456789.":
            start = i
            points = 0
            while i < n and s[i] in "0123456789.":
                if s[i] == ".":
                    points = points + 1
                i = i + 1
            text = s[start:i]
            if points > 1 or text[0] == "." or text[len(text) - 1] == ".":
                return [False, "Not a number: " + text]
            out = out + [["num", frac.decimal(text), text]]
        elif c in "+-*/":
            out = out + [["op", c, c]]
            i = i + 1
        elif c == "(" or c == ")":
            out = out + [[c, c, c]]
            i = i + 1
        elif s[i:i + 3] == "ans":
            out = out + [["ans", "ans", "ans"]]
            i = i + 3
        else:
            return [False, "Unexpected character: " + c]
    return [True, out]

def rank(t):
    if t[0] == "neg":
        return 3
    if t[1] == "*" or t[1] == "/":
        return 2
    return 1

def postfix(toks):
    out = []
    ops = []
    want_number = True
    for t in toks:
        if want_number:
            if t[0] == "num" or t[0] == "ans":
                out = out + [t]
                want_number = False
            elif t[0] == "(":
                ops = ops + [t]
            elif t[0] == "op" and t[1] == "-":
                ops = ops + [["neg", "-", "-"]]
            else:
                return [False, "A number is missing before " + t[2] + "."]
        elif t[0] == "op":
            while len(ops) > 0 and ops[len(ops) - 1][0] != "(" and rank(ops[len(ops) - 1]) >= rank(t):
                out = out + [ops[len(ops) - 1]]
                ops = ops[0:len(ops) - 1]
            ops = ops + [t]
            want_number = True
        elif t[0] == ")":
            while len(ops) > 0 and ops[len(ops) - 1][0] != "(":
                out = out + [ops[len(ops) - 1]]
                ops = ops[0:len(ops) - 1]
            if len(ops) == 0:
                return [False, "There is no ( for this )."]
            ops = ops[0:len(ops) - 1]
        else:
            return [False, "An operator is missing before " + t[2] + "."]
    if want_number:
        return [False, "The expression is incomplete."]
    while len(ops) > 0:
        if ops[len(ops) - 1][0] == "(":
            return [False, "A ( is not closed."]
        out = out + [ops[len(ops) - 1]]
        ops = ops[0:len(ops) - 1]
    return [True, out]

def evaluate(post, ans):
    stack = []
    for t in post:
        if t[0] == "num":
            stack = stack + [t[1]]
        elif t[0] == "ans":
            if ans == None:
                return [False, "There is no answer yet for ans."]
            stack = stack + [ans]
        elif t[0] == "neg":
            stack = stack[0:len(stack) - 1] + [frac.negated(stack[len(stack) - 1])]
        else:
            x = stack[len(stack) - 2]
            y = stack[len(stack) - 1]
            stack = stack[0:len(stack) - 2]
            if t[1] == "+":
                z = frac.plus(x, y)
            elif t[1] == "-":
                z = frac.minus(x, y)
            elif t[1] == "*":
                z = frac.times(x, y)
            else:
                if y[0] == 0:
                    return [False, "Division by zero."]
                z = frac.over(x, y)
            stack = stack + [z]
    return [True, stack[0]]

def run(text, ans):
    r = tokens(text)
    if r[0]:
        r = postfix(r[1])
    if r[0]:
        r = evaluate(r[1], ans)
    return r
```

### frac.eml

```eml
# P007 calculator - exact fractions. A number is a list [n, d]: n / d in
# lowest terms with d > 0, so 0.1 is [1, 10] and 0.1 + 0.2 is exactly [3, 10].
# Integers have no size limit, but EML has no //, and a / b goes through a
# float that cannot hold a large quotient exactly, so whole-number division
# is written out below.

def quotient(a, b):
    # a // b for whole numbers a >= 0 and b > 0, exact at any size. Long
    # division by doubling: take away the largest b * 2^k that still fits.
    0 => q
    while a >= b:
        b => m
        1 => k
        while m + m <= a:
            m + m => m
            k + k => k
        a - m => a
        q + k => q
    return q

def gcd(a, b):
    # Greatest common divisor of whole numbers a, b >= 0 (Euclid).
    while b != 0:
        a % b => r
        b => a
        r => b
    return a

def make(n, d):
    # n / d in lowest terms with a positive denominator (d != 0).
    if d < 0:
        0 - n => n
        0 - d => d
    abs(n) => a
    gcd(a, d) => g
    if n < 0:
        return [0 - quotient(a, g), quotient(d, g)]
    return [quotient(a, g), quotient(d, g)]

def plus(x, y):
    return make(x[0] * y[1] + y[0] * x[1], x[1] * y[1])

def minus(x, y):
    return make(x[0] * y[1] - y[0] * x[1], x[1] * y[1])

def times(x, y):
    return make(x[0] * y[0], x[1] * y[1])

def over(x, y):
    # x / y; the caller has checked that y is not zero.
    return make(x[0] * y[1], x[1] * y[0])

def negated(x):
    return [0 - x[0], x[1]]

def decimal(text):
    # A number typed as digits with at most one point ("12", "0.25") as a
    # fraction, exactly: 12.345 is 12345 / 1000.
    "" => digits
    1 => scale
    False => point
    for c in text:
        if c == ".":
            True => point
        else:
            digits + c => digits
            if point:
                scale * 10 => scale
    return make(int(digits), scale)

def with_point(m, k):
    # The whole number m >= 0 written with k digits after a point.
    str(m) => s
    while len(s) <= k:
        "0" + s => s
    if k == 0:
        return s
    return s[0:len(s) - k] + "." + s[len(s) - k:len(s)]

def shown(x):
    # An integer as it is; a fraction whose decimal ends, as that decimal
    # (5/2 is 2.5); any other fraction as n/d with its value to ten places,
    # rounded half up ("1/3, about 0.3333333333").
    "" => sign
    if x[0] < 0:
        "-" => sign
        negated(x) => x
    if x[1] == 1:
        return sign + str(x[0])
    # The decimal ends if the denominator divides a power of ten.
    1 => p
    0 => k
    while p % x[1] != 0 and k < 60:
        p * 10 => p
        k + 1 => k
    if p % x[1] == 0:
        return sign + with_point(x[0] * quotient(p, x[1]), k)
    10000000000 => ten10
    quotient(2 * x[0] * ten10 + x[1], 2 * x[1]) => t
    return sign + str(x[0]) + "/" + str(x[1]) + ", about " + sign + with_point(t, 10)
```

Python projection of frac.eml:

```python
def quotient(a, b):
    q = 0
    while a >= b:
        m = b
        k = 1
        while m + m <= a:
            m = m + m
            k = k + k
        a = a - m
        q = q + k
    return q

def gcd(a, b):
    while b != 0:
        r = a % b
        a = b
        b = r
    return a

def make(n, d):
    if d < 0:
        n = 0 - n
        d = 0 - d
    a = abs(n)
    g = gcd(a, d)
    if n < 0:
        return [0 - quotient(a, g), quotient(d, g)]
    return [quotient(a, g), quotient(d, g)]

def plus(x, y):
    return make(x[0] * y[1] + y[0] * x[1], x[1] * y[1])

def minus(x, y):
    return make(x[0] * y[1] - y[0] * x[1], x[1] * y[1])

def times(x, y):
    return make(x[0] * y[0], x[1] * y[1])

def over(x, y):
    return make(x[0] * y[1], x[1] * y[0])

def negated(x):
    return [0 - x[0], x[1]]

def decimal(text):
    digits = ""
    scale = 1
    point = False
    for c in text:
        if c == ".":
            point = True
        else:
            digits = digits + c
            if point:
                scale = scale * 10
    return make(int(digits), scale)

def with_point(m, k):
    s = str(m)
    while len(s) <= k:
        s = "0" + s
    if k == 0:
        return s
    return s[0:len(s) - k] + "." + s[len(s) - k:len(s)]

def shown(x):
    sign = ""
    if x[0] < 0:
        sign = "-"
        x = negated(x)
    if x[1] == 1:
        return sign + str(x[0])
    p = 1
    k = 0
    while p % x[1] != 0 and k < 60:
        p = p * 10
        k = k + 1
    if p % x[1] == 0:
        return sign + with_point(x[0] * quotient(p, x[1]), k)
    ten10 = 10000000000
    t = quotient(2 * x[0] * ten10 + x[1], 2 * x[1])
    return sign + str(x[0]) + "/" + str(x[1]) + ", about " + sign + with_point(t, 10)
```

## README

# P007 - Calculator

Type an expression with `+ - * /`, parentheses and a minus in front, and it
is worked out exactly: every number is a fraction, so `0.1 + 0.2` is `0.3`
and `1 / 3 * 3` is `1`. `ans` stands for the last answer, `history` lists what
was worked out, `clear` forgets it, `quit` ends. A prompt rather than a menu,
which is what a calculator is; the history lives while the program runs.

- `main.eml` - the prompt, the commands and the history
- `expr.eml` - three steps from text to answer: cut the text into numbers,
  operators and parentheses; put them in postfix order with the
  shunting-yard method, checking the shape of the expression on the way
  (minus in front binds tightest, then `* /`, then `+ -`, all left to
  right); work the postfix out on a stack
- `frac.eml` - exact fractions kept in lowest terms, and how an answer is
  shown: an integer as it is, a fraction whose decimal ends as that decimal
  (5/2 is 2.5), any other fraction as `n/d, about` its value to ten places,
  rounded half up

Integers have no size limit (`123456789 * 987654321` is exact), but EML has
no `//`, and `a / b` goes through a float that cannot hold a large quotient,
so whole-number division is written out as long division by doubling.

What is reported instead of an answer: a number that is not one (`1..2`,
`.5`), a character that is not part of an expression, a missing number or
operator (`3 4`, `*3`, `()`), an unclosed or unopened parenthesis, an
expression that stops early (`2 +`), division by zero, and `ans` before any
answer.

Sessions: `sessions/basic.in` works out integers, decimals that floats get
wrong, fractions that do not end, `ans`, a minus in front, precedence, a big
product and its exact quotient, and shows and clears the history;
`sessions/bad-input.in` types every kind of error above, then a good
expression.

Built on the verified corpus cases `rpn-evaluator` (working out an
expression in postfix order on a stack) and `simple-stack` (a list used as a
stack: push onto the end, pop from the end).
