Project 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.

3 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-09-29

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

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).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
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.
What was typed (15 lines)
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

basic

interpreter: byte-equal
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.
What was typed (16 lines)
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

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

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 (main.py)
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 (expr.py)
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 (frac.py)
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)

Built on these corpus cases