Project P062

Expression calculator

Expressions typed the usual way - + - * /, whole powers with ^, brackets and minus signs - turned into postfix by Dijkstra's shunting-yard algorithm and worked out on a stack, both shown step by step, in exact fractions; postfix can be typed directly too.

3 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-14

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 the usual way, with + - * /, ^ for whole powers, brackets and minus signs, like (1 + 2) * -3 / 4. The calculator turns it into postfix order with Dijkstra's shunting-yard algorithm, works the postfix out on a stack, and can show both step by step. Every value is an exact fraction. Postfix can also be typed in directly.

  • main.eml - the menu, reading expressions and postfix, the step tables and the answer
  • expr.eml - tokens, the shunting yard, and the stack that works postfix out
  • frac.eml - exact fractions, from P007 by way of P038, P044, P056 and P060

How each part works:

  • Tokens: numbers (whole or decimal), the operators, and brackets. A minus sign at the start, after an operator or after "(" is a sign, shown as neg; anywhere else it subtracts.
  • Shunting yard. A number goes straight to the output. An operator first moves to the output every operator waiting on the stack that binds tighter, or as tight when it groups to the left, and then waits itself. "(" waits, and ")" moves operators out until its "(".
  • Precedence, from loosest: + and -, then * and /, then the sign, then ^. So -2^2 is -(2^2) = -4, and (-2)^2 is 4. ^ groups to the right, so 2 ^ 3 ^ 2 is 2^9 = 512; the rest group to the left, so 8 - 3 - 2 is 3.
  • Stack. As in the corpus case rpn-evaluator, a number is pushed, and an operator pops what it needs - two numbers, or one for the sign - and pushes its result. One number left at the end is the answer.
  • The answer is a fraction in lowest terms, with its decimal. A decimal that does not end is rounded to six places and marked with ~.
  • A power must be a whole number no larger than 20. Dividing by zero and zero to a negative power are refused with the reason.

What is checked: menu choices 1 to 4; expressions of up to 80 characters with numbers, + - * / ^, brackets and spaces; brackets that match; operators with numbers to work on; postfix tokens separated by spaces. An empty answer cancels.

Sessions: sessions/basic.in gives: - (1 + 2) * -3 / 4 step by step, = -9/4 = -2.25; - 2 ^ 3 ^ 2 step by step, grouping to the right, = 512; - 1/3 + 1/6 = 1/2, -2^2 + (-2)^2 = 0, 10 / 4 - 0.5 = 2, and 1/3, which is ~0.333333; - the postfix 3 4 + 2 *, = 14.

sessions/bad-input.in gives: - menu choices 0 and x, and an empty expression; - unbalanced brackets both ways, a $, the number 1.2.3, "2 +" and "2 3"; - division by zero, 2 ^ 0.5, 2 ^ 21 and 0 ^ -1; - an empty pair of brackets, and 81 characters; - the postfix "3 +", "1 x" and an empty one.

Built on the verified corpus case rpn-evaluator (postfix worked out by pushing numbers and popping two for each operator).

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
== Expression calculator ==
Exact fractions: + - * /, ^ for whole powers, brackets and minus signs, like (1 + 2) * -3 / 4.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 0
Pick a number from 1 to 4.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> x
Pick a number from 1 to 4.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 1
expression> 
Cancelled.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> (1 + 2
Not an expression: a ( that is never closed.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 1 + 2)
Not an expression: a ) with no ( before it.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 2 $ 3
Cannot read the character $.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 1.2.3
Cannot read the number 1.2.3.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 2 +
Cannot be worked out: + without two numbers to work on.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 2 3
Cannot be worked out: numbers with no operator between them.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 1 / (2 - 2)
Cannot be worked out: division by zero.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 2 ^ 0.5
Cannot be worked out: a power that is not a whole number: 1/2.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 2 ^ 21
Cannot be worked out: a power above 20.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 0 ^ -1
Cannot be worked out: zero to a negative power.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> ()
There is nothing to work out.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
Keep an expression to 80 characters.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 3
postfix, tokens with spaces between (neg for a minus sign)> 3 +
Postfix: 3 +
Stack - each postfix token, then the stack:
  3     3
Cannot be worked out: + without two numbers to work on.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 3
postfix, tokens with spaces between (neg for a minus sign)> 1 x
Not a number or an operator: x.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 3
postfix, tokens with spaces between (neg for a minus sign)> 
Cancelled.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 4
Bye.
What was typed (35 lines)
0
x
1

2
(1 + 2
2
1 + 2)
2
2 $ 3
2
1.2.3
2
2 +
2
2 3
2
1 / (2 - 2)
2
2 ^ 0.5
2
2 ^ 21
2
0 ^ -1
2
()
2
1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
3
3 +
3
1 x
3

4

basic

interpreter: byte-equal
== Expression calculator ==
Exact fractions: + - * /, ^ for whole powers, brackets and minus signs, like (1 + 2) * -3 / 4.

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 1
expression> (1 + 2) * -3 / 4
Postfix: 1 2 + 3 neg * 4 /
Shunting yard - each token, then the output so far and the operators waiting:
  (                                  | (
  1     1                            | (
  +     1                            | ( +
  2     1 2                          | ( +
  )     1 2 +                        |
  *     1 2 +                        | *
  neg   1 2 +                        | * neg
  3     1 2 + 3                      | * neg
  /     1 2 + 3 neg *                | /
  4     1 2 + 3 neg * 4              | /
Stack - each postfix token, then the stack:
  1     1
  2     1 2
  +     3
  3     3 3
  neg   3 -3
  *     -9
  4     -9 4
  /     -9/4
= -9/4 = -2.25

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 1
expression> 2 ^ 3 ^ 2
Postfix: 2 3 2 ^ ^
Shunting yard - each token, then the output so far and the operators waiting:
  2     2                            |
  ^     2                            | ^
  3     2 3                          | ^
  ^     2 3                          | ^ ^
  2     2 3 2                        | ^ ^
Stack - each postfix token, then the stack:
  2     2
  3     2 3
  2     2 3 2
  ^     2 9
  ^     512
= 512

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 1/3 + 1/6
= 1/2 = 0.5

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> -2^2 + (-2)^2
= 0

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 10 / 4 - 0.5
= 2

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 2
expression> 1/3
= 1/3 = ~0.333333

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 3
postfix, tokens with spaces between (neg for a minus sign)> 3 4 + 2 *
Postfix: 3 4 + 2 *
Stack - each postfix token, then the stack:
  3     3
  4     3 4
  +     7
  2     7 2
  *     14
= 14

1) step by step  2) just the answer  3) type postfix  4) quit
choice> 4
Bye.
What was typed (15 lines)
1
(1 + 2) * -3 / 4
1
2 ^ 3 ^ 2
2
1/3 + 1/6
2
-2^2 + (-2)^2
2
10 / 4 - 0.5
2
1/3
3
3 4 + 2 *
4

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
# P062 expression calculator: type an expression the usual way - with + - *
# /, ^ for whole powers, brackets and minus signs - and see it turned into
# postfix order by the shunting-yard algorithm and worked out on a stack,
# step by step, in exact fractions. Postfix can be typed directly too.
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]

def words(s):
    [] => out
    "" => word
    for c in s + " ":
        if c == " ":
            if word != "":
                out + [word] => out
            "" => word
        else:
            word + c => word
    return out

def joined(ts):
    "" => s
    for t in ts:
        if s != "":
            s + " " => s
        s + expr.shown(t) => s
    return s

def numbers(xs):
    "" => s
    for x in xs:
        if s != "":
            s + " " => s
        s + frac.text(x) => s
    return s

def padded(s, width):
    while len(s) < width:
        s + " " => s
    return s

def answer(x):
    # 7/2 = 3.5; a whole number as it is; a long decimal rounded, marked ~.
    if x[1] == 1:
        return frac.text(x)
    frac.decimal(x, 6) => d
    if len(d) > 6 and d[0:6] == "about ":
        "~" + d[6:len(d)] => d
    return frac.text(x) + " = " + d

def show_steps(postfix_steps, eval_steps):
    if len(postfix_steps) > 0:
        "Shunting yard - each token, then the output so far and the operators waiting:" ^0
        for s in postfix_steps:
            "  " + padded(expr.shown(s[0]), 6) + padded(joined(s[1]), 28) + " |" => line
            if len(s[2]) > 0:
                line + " " + joined(s[2]) => line
            line ^0
    "Stack - each postfix token, then the stack:" ^0
    for s in eval_steps:
        ("  " + padded(expr.shown(s[0]), 6) + numbers(s[1])) ^0

def work_out(steps):
    trim(input("expression> ")) => s
    if s == "":
        "Cancelled." ^0
        return
    if len(s) > 80:
        "Keep an expression to 80 characters." ^0
        return
    expr.tokens(s) => ts
    if len(ts) > 0 and ts[0][0] == "bad":
        ("Cannot read " + ts[0][1] + ".") ^0
        return
    expr.to_postfix(ts) => p
    if p[2] != "":
        ("Not an expression: " + p[2] + ".") ^0
        return
    if len(p[0]) == 0:
        "There is nothing to work out." ^0
        return
    run(p[0], p[1], steps)

def run(postfix, postfix_steps, steps):
    expr.evaluate(postfix) => r
    if steps:
        ("Postfix: " + joined(postfix)) ^0
        show_steps(postfix_steps, r[1])
    if r[2] != "":
        ("Cannot be worked out: " + r[2] + ".") ^0
        return
    ("= " + answer(r[0])) ^0

def postfix_input():
    words(trim(input("postfix, tokens with spaces between (neg for a minus sign)> "))) => ws
    if len(ws) == 0:
        "Cancelled." ^0
        return
    [] => ts
    for w in ws:
        if w == "+" or w == "-" or w == "*" or w == "/" or w == "^" or w == "neg":
            ts + [["op", w]] => ts
        else:
            frac.parse(w) => x
            if len(x) == 0:
                ("Not a number or an operator: " + w + ".") ^0
                return
            ts + [["num", x]] => ts
    run(ts, [], True)

"== Expression calculator ==" ^0
"Exact fractions: + - * /, ^ for whole powers, brackets and minus signs, like (1 + 2) * -3 / 4." ^0
True => running
while running:
    "" ^0
    "1) step by step  2) just the answer  3) type postfix  4) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        work_out(True)
    elif choice == "2":
        work_out(False)
    elif choice == "3":
        postfix_input()
    elif choice == "4":
        False => running
    else:
        "Pick a number from 1 to 4." ^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]

def words(s):
    out = []
    word = ""
    for c in s + " ":
        if c == " ":
            if word != "":
                out = out + [word]
            word = ""
        else:
            word = word + c
    return out

def joined(ts):
    s = ""
    for t in ts:
        if s != "":
            s = s + " "
        s = s + expr.shown(t)
    return s

def numbers(xs):
    s = ""
    for x in xs:
        if s != "":
            s = s + " "
        s = s + frac.text(x)
    return s

def padded(s, width):
    while len(s) < width:
        s = s + " "
    return s

def answer(x):
    if x[1] == 1:
        return frac.text(x)
    d = frac.decimal(x, 6)
    if len(d) > 6 and d[0:6] == "about ":
        d = "~" + d[6:len(d)]
    return frac.text(x) + " = " + d

def show_steps(postfix_steps, eval_steps):
    if len(postfix_steps) > 0:
        print("Shunting yard - each token, then the output so far and the operators waiting:")
        for s in postfix_steps:
            line = "  " + padded(expr.shown(s[0]), 6) + padded(joined(s[1]), 28) + " |"
            if len(s[2]) > 0:
                line = line + " " + joined(s[2])
            print(line)
    print("Stack - each postfix token, then the stack:")
    for s in eval_steps:
        print("  " + padded(expr.shown(s[0]), 6) + numbers(s[1]))

def work_out(steps):
    s = trim(input("expression> "))
    if s == "":
        print("Cancelled.")
        return
    if len(s) > 80:
        print("Keep an expression to 80 characters.")
        return
    ts = expr.tokens(s)
    if len(ts) > 0 and ts[0][0] == "bad":
        print("Cannot read " + ts[0][1] + ".")
        return
    p = expr.to_postfix(ts)
    if p[2] != "":
        print("Not an expression: " + p[2] + ".")
        return
    if len(p[0]) == 0:
        print("There is nothing to work out.")
        return
    run(p[0], p[1], steps)

def run(postfix, postfix_steps, steps):
    r = expr.evaluate(postfix)
    if steps:
        print("Postfix: " + joined(postfix))
        show_steps(postfix_steps, r[1])
    if r[2] != "":
        print("Cannot be worked out: " + r[2] + ".")
        return
    print("= " + answer(r[0]))

def postfix_input():
    ws = words(trim(input("postfix, tokens with spaces between (neg for a minus sign)> ")))
    if len(ws) == 0:
        print("Cancelled.")
        return
    ts = []
    for w in ws:
        if w == "+" or w == "-" or w == "*" or w == "/" or w == "^" or w == "neg":
            ts = ts + [["op", w]]
        else:
            x = frac.parse(w)
            if len(x) == 0:
                print("Not a number or an operator: " + w + ".")
                return
            ts = ts + [["num", x]]
    run(ts, [], True)

print("== Expression calculator ==")
print("Exact fractions: + - * /, ^ for whole powers, brackets and minus signs, like (1 + 2) * -3 / 4.")
running = True
while running:
    print("")
    print("1) step by step  2) just the answer  3) type postfix  4) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        work_out(True)
    elif choice == "2":
        work_out(False)
    elif choice == "3":
        postfix_input()
    elif choice == "4":
        running = False
    else:
        print("Pick a number from 1 to 4.")
print("Bye.")

expr.eml

eml
# P062 expression calculator - tokens, the shunting-yard conversion from the
# usual (infix) order to postfix, and the stack that works postfix out.
# A token is ["num", fraction], ["op", "+"], ["(", ""] or [")", ""]. The
# operators: + - * / and ^ (a whole-number power), and "neg", a minus sign
# in front of a number or a bracket.
import frac

def precedence(op):
    if op == "+" or op == "-":
        return 1
    if op == "*" or op == "/":
        return 2
    if op == "neg":
        return 3
    return 4

def right_assoc(op):
    # 2 ^ 3 ^ 2 is 2 ^ (3 ^ 2), and - - 3 is -(-3); the rest group left.
    return op == "^" or op == "neg"

def tokens(s):
    # The tokens of s, or [["bad", why]].
    [] => out
    0 => i
    while i < len(s):
        s[i] => c
        if c == " ":
            i + 1 => i
        elif (c >= "0" and c <= "9") or c == ".":
            i => j
            while j < len(s) and ((s[j] >= "0" and s[j] <= "9") or s[j] == "."):
                j + 1 => j
            frac.parse(s[i:j]) => x
            if len(x) == 0:
                return [["bad", "the number " + s[i:j]]]
            out + [["num", x]] => out
            j => i
        elif c == "+" or c == "-" or c == "*" or c == "/" or c == "^":
            # A minus with nothing, an operator or "(" before it is a sign.
            if c == "-" and (len(out) == 0 or out[len(out) - 1][0] == "op" or out[len(out) - 1][0] == "("):
                out + [["op", "neg"]] => out
            else:
                out + [["op", c]] => out
            i + 1 => i
        elif c == "(" or c == ")":
            out + [[c, ""]] => out
            i + 1 => i
        else:
            return [["bad", "the character " + c]]
    return out

def shown(t):
    if t[0] == "num":
        return frac.text(t[1])
    if t[0] == "op":
        if t[1] == "neg":
            return "neg"
        return t[1]
    return t[0]

def to_postfix(ts):
    # Dijkstra's shunting yard. Numbers go straight to the output; an
    # operator first moves to the output every operator on the stack that
    # binds tighter (or as tight, when it groups left), then waits on the
    # stack; "(" waits, and ")" moves operators out until its "(". Returns
    # [postfix tokens, steps, problem]: each step [token, output, stack].
    [] => output
    [] => stack
    [] => steps
    "" => problem
    for t in ts:
        if t[0] == "num":
            output + [t] => output
        elif t[0] == "op":
            True => moving
            while moving and len(stack) > 0 and stack[len(stack) - 1][0] == "op" and t[1] != "neg":
                stack[len(stack) - 1][1] => top
                if precedence(top) > precedence(t[1]) or (precedence(top) == precedence(t[1]) and not right_assoc(t[1])):
                    output + [stack[len(stack) - 1]] => output
                    stack[0:len(stack) - 1] => stack
                else:
                    False => moving
            stack + [t] => stack
        elif t[0] == "(":
            stack + [t] => stack
        else:
            while len(stack) > 0 and stack[len(stack) - 1][0] != "(":
                output + [stack[len(stack) - 1]] => output
                stack[0:len(stack) - 1] => stack
            if len(stack) == 0:
                "a ) with no ( before it" => problem
                return [output, steps, problem]
            stack[0:len(stack) - 1] => stack
        steps + [[t, output, stack]] => steps
    while len(stack) > 0:
        if stack[len(stack) - 1][0] == "(":
            "a ( that is never closed" => problem
            return [output, steps, problem]
        output + [stack[len(stack) - 1]] => output
        stack[0:len(stack) - 1] => stack
    return [output, steps, problem]

def power(x, n):
    # x to a whole power n, exactly; the caller has checked x != 0 when n < 0.
    [1, 1] => r
    abs(n) => k
    for i in [1:k]:
        frac.times(r, x) => r
    if n < 0:
        return frac.over([1, 1], r)
    return r

def apply(op, a, b):
    # [value, problem] for a op b.
    if op == "+":
        return [frac.plus(a, b), ""]
    if op == "-":
        return [frac.minus(a, b), ""]
    if op == "*":
        return [frac.times(a, b), ""]
    if op == "/":
        if frac.is_zero(b):
            return [[0, 1], "division by zero"]
        return [frac.over(a, b), ""]
    if b[1] != 1:
        return [[0, 1], "a power that is not a whole number: " + frac.text(b)]
    if abs(b[0]) > 20:
        return [[0, 1], "a power above 20"]
    if frac.is_zero(a) and b[0] < 0:
        return [[0, 1], "zero to a negative power"]
    return [power(a, b[0]), ""]

def evaluate(postfix):
    # The corpus case rpn-evaluator's stack: push numbers; an operator pops
    # what it needs - two, or one for neg - and pushes its result. Returns
    # [value, steps, problem]: each step [token, stack after].
    [] => stack
    [] => steps
    for t in postfix:
        if t[0] == "num":
            stack + [t[1]] => stack
        elif t[1] == "neg":
            if len(stack) < 1:
                return [[0, 1], steps, "a minus sign with no number after it"]
            frac.minus([0, 1], stack[len(stack) - 1]) => stack[len(stack) - 1]
        else:
            if len(stack) < 2:
                return [[0, 1], steps, t[1] + " without two numbers to work on"]
            stack[len(stack) - 2] => a
            stack[len(stack) - 1] => b
            stack[0:len(stack) - 2] => stack
            apply(t[1], a, b) => res
            if res[1] != "":
                return [[0, 1], steps, res[1]]
            stack + [res[0]] => stack
        steps + [[t, stack[0:len(stack)]]] => steps
    if len(stack) != 1:
        return [[0, 1], steps, "numbers with no operator between them"]
    return [stack[0], steps, ""]
Python projection (expr.py)
import frac

def precedence(op):
    if op == "+" or op == "-":
        return 1
    if op == "*" or op == "/":
        return 2
    if op == "neg":
        return 3
    return 4

def right_assoc(op):
    return op == "^" or op == "neg"

def tokens(s):
    out = []
    i = 0
    while i < len(s):
        c = s[i]
        if c == " ":
            i = i + 1
        elif c >= "0" and c <= "9" or c == ".":
            j = i
            while j < len(s) and (s[j] >= "0" and s[j] <= "9" or s[j] == "."):
                j = j + 1
            x = frac.parse(s[i:j])
            if len(x) == 0:
                return [["bad", "the number " + s[i:j]]]
            out = out + [["num", x]]
            i = j
        elif c == "+" or c == "-" or c == "*" or c == "/" or c == "^":
            if c == "-" and (len(out) == 0 or out[len(out) - 1][0] == "op" or out[len(out) - 1][0] == "("):
                out = out + [["op", "neg"]]
            else:
                out = out + [["op", c]]
            i = i + 1
        elif c == "(" or c == ")":
            out = out + [[c, ""]]
            i = i + 1
        else:
            return [["bad", "the character " + c]]
    return out

def shown(t):
    if t[0] == "num":
        return frac.text(t[1])
    if t[0] == "op":
        if t[1] == "neg":
            return "neg"
        return t[1]
    return t[0]

def to_postfix(ts):
    output = []
    stack = []
    steps = []
    problem = ""
    for t in ts:
        if t[0] == "num":
            output = output + [t]
        elif t[0] == "op":
            moving = True
            while moving and len(stack) > 0 and stack[len(stack) - 1][0] == "op" and t[1] != "neg":
                top = stack[len(stack) - 1][1]
                if precedence(top) > precedence(t[1]) or precedence(top) == precedence(t[1]) and not right_assoc(t[1]):
                    output = output + [stack[len(stack) - 1]]
                    stack = stack[0:len(stack) - 1]
                else:
                    moving = False
            stack = stack + [t]
        elif t[0] == "(":
            stack = stack + [t]
        else:
            while len(stack) > 0 and stack[len(stack) - 1][0] != "(":
                output = output + [stack[len(stack) - 1]]
                stack = stack[0:len(stack) - 1]
            if len(stack) == 0:
                problem = "a ) with no ( before it"
                return [output, steps, problem]
            stack = stack[0:len(stack) - 1]
        steps = steps + [[t, output, stack]]
    while len(stack) > 0:
        if stack[len(stack) - 1][0] == "(":
            problem = "a ( that is never closed"
            return [output, steps, problem]
        output = output + [stack[len(stack) - 1]]
        stack = stack[0:len(stack) - 1]
    return [output, steps, problem]

def power(x, n):
    r = [1, 1]
    k = abs(n)
    for i in range(1, k+1):
        r = frac.times(r, x)
    if n < 0:
        return frac.over([1, 1], r)
    return r

def apply(op, a, b):
    if op == "+":
        return [frac.plus(a, b), ""]
    if op == "-":
        return [frac.minus(a, b), ""]
    if op == "*":
        return [frac.times(a, b), ""]
    if op == "/":
        if frac.is_zero(b):
            return [[0, 1], "division by zero"]
        return [frac.over(a, b), ""]
    if b[1] != 1:
        return [[0, 1], "a power that is not a whole number: " + frac.text(b)]
    if abs(b[0]) > 20:
        return [[0, 1], "a power above 20"]
    if frac.is_zero(a) and b[0] < 0:
        return [[0, 1], "zero to a negative power"]
    return [power(a, b[0]), ""]

def evaluate(postfix):
    stack = []
    steps = []
    for t in postfix:
        if t[0] == "num":
            stack = stack + [t[1]]
        elif t[1] == "neg":
            if len(stack) < 1:
                return [[0, 1], steps, "a minus sign with no number after it"]
            stack[len(stack) - 1] = frac.minus([0, 1], stack[len(stack) - 1])
        else:
            if len(stack) < 2:
                return [[0, 1], steps, t[1] + " without two numbers to work on"]
            a = stack[len(stack) - 2]
            b = stack[len(stack) - 1]
            stack = stack[0:len(stack) - 2]
            res = apply(t[1], a, b)
            if res[1] != "":
                return [[0, 1], steps, res[1]]
            stack = stack + [res[0]]
        steps = steps + [[t, stack[0:len(stack)]]]
    if len(stack) != 1:
        return [[0, 1], steps, "numbers with no operator between them"]
    return [stack[0], steps, ""]

frac.eml

eml
# P062 expression calculator - exact fractions, from P007 (calculator) by
# way of P038, P044, P056 and P060.
# A number is a list [n, d]: n / d in lowest terms with d > 0. 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.

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):
    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 is_zero(x):
    return x[0] == 0

def digits_value(s):
    # The value of a string of 1 to 9 digits, otherwise -1.
    if s == "" or len(s) > 9:
        return -1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return -1
        n * 10 + int(c) => n
    return n

def parse(s):
    # "3", "-2", "3/4", "-0.25" as a fraction; [] if it is not a number.
    1 => sign
    if len(s) > 0 and s[0] == "-":
        -1 => sign
        s[1:len(s)] => s
    0 => k
    while k < len(s) and s[k] != "/" and s[k] != ".":
        k + 1 => k
    if k == len(s):
        digits_value(s) => n
        if n < 0:
            return []
        return make(sign * n, 1)
    digits_value(s[0:k]) => whole
    s[k + 1:len(s)] => rest
    digits_value(rest) => part
    if whole < 0 or part < 0:
        return []
    if s[k] == "/":
        if part == 0:
            return []
        return make(sign * whole, part)
    # a decimal point: 0.25 is 25 / 100
    1 => scale
    for c in rest:
        scale * 10 => scale
    return make(sign * (whole * scale + part), scale)

def less(x, y):
    return x[0] * y[1] < y[0] * x[1]

def decimal(x, places):
    # x to the given number of decimal places, halves rounded away from zero,
    # with "about " in front when the value does not end there exactly.
    "" => sign
    abs(x[0]) => n
    if x[0] < 0:
        "-" => sign
    1 => scale
    for k in [1:places]:
        scale * 10 => scale
    quotient(2 * n * scale + x[1], 2 * x[1]) => t
    "" => about
    if (n * scale) % x[1] != 0:
        "about " => about
    str(quotient(t, scale)) => whole
    str(t % scale) => part
    while len(part) < places:
        "0" + part => part
    # drop trailing zeros of an exact value
    if about == "":
        while len(part) > 0 and part[len(part) - 1] == "0":
            part[0:len(part) - 1] => part
    if t == 0:
        "" => sign
    if part == "":
        return about + sign + whole
    return about + sign + whole + "." + part

def text(x):
    # "3", "-3", "3/4", "-3/4".
    if x[1] == 1:
        return str(x[0])
    return str(x[0]) + "/" + str(x[1])
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 is_zero(x):
    return x[0] == 0

def digits_value(s):
    if s == "" or len(s) > 9:
        return -1
    n = 0
    for c in s:
        if not c in "0123456789":
            return -1
        n = n * 10 + int(c)
    return n

def parse(s):
    sign = 1
    if len(s) > 0 and s[0] == "-":
        sign = -1
        s = s[1:len(s)]
    k = 0
    while k < len(s) and s[k] != "/" and s[k] != ".":
        k = k + 1
    if k == len(s):
        n = digits_value(s)
        if n < 0:
            return []
        return make(sign * n, 1)
    whole = digits_value(s[0:k])
    rest = s[k + 1:len(s)]
    part = digits_value(rest)
    if whole < 0 or part < 0:
        return []
    if s[k] == "/":
        if part == 0:
            return []
        return make(sign * whole, part)
    scale = 1
    for c in rest:
        scale = scale * 10
    return make(sign * (whole * scale + part), scale)

def less(x, y):
    return x[0] * y[1] < y[0] * x[1]

def decimal(x, places):
    sign = ""
    n = abs(x[0])
    if x[0] < 0:
        sign = "-"
    scale = 1
    for k in range(1, places+1):
        scale = scale * 10
    t = quotient(2 * n * scale + x[1], 2 * x[1])
    about = ""
    if n * scale % x[1] != 0:
        about = "about "
    whole = str(quotient(t, scale))
    part = str(t % scale)
    while len(part) < places:
        part = "0" + part
    if about == "":
        while len(part) > 0 and part[len(part) - 1] == "0":
            part = part[0:len(part) - 1]
    if t == 0:
        sign = ""
    if part == "":
        return about + sign + whole
    return about + sign + whole + "." + part

def text(x):
    if x[1] == 1:
        return str(x[0])
    return str(x[0]) + "/" + str(x[1])

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.