<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P056-mini-spreadsheet/ | updated: 2026-10-10 -->

# P056 Mini spreadsheet

Cells A1 to F9 holding numbers, labels and formulas - + - * / with parentheses, cell references, SUM, MIN, MAX and AVG over ranges - kept as exact fractions. Every change recalculates the sheet in dependency order (Kahn's algorithm), and formulas caught in a cycle are found and marked instead of looping.

EML-P project `projects/mini-spreadsheet` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/mini-spreadsheet` runs it and `eml project verify projects/mini-spreadsheet` 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: topological-sort (https://efficientnewlanguage.org/cases/107-topological-sort/).

## Sessions

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

Input:

```text
0
x
1
G1
A0

1
A1

1
A1
=1+
1
A1
=SUM(B1
1
A1
=A1+1
1
A2
=B1/C1
1
A3
label
1
A4
=A3*2
1
A5
=SUM(A3:A4, B9)
1
A6
=1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1
1
A6
=AVG(B1:B9)
2
A1
3
4
5
7
```

Screen:

```text
== Mini spreadsheet ==
Cells A1 to F9 hold numbers, labels, or formulas such as =B2*C2 or =SUM(D2:D4).

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 0
Pick a number from 1 to 7.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> x
Pick a number from 1 to 7.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> G1
Type a cell from A1 to F9, like B3.
cell> A0
Type a cell from A1 to F9, like B3.
cell> 
Cancelled.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A1
content of A1 (a number, a label, or =formula)> 
Cancelled - use 2) to empty a cell.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A1
content of A1 (a number, a label, or =formula)> =1+
Not a formula: it has something missing where a number, a cell or ( should be. Nothing was changed.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A1
content of A1 (a number, a label, or =formula)> =SUM(B1
Not a formula: it has SUM( with no , between two cells or no ) at the end. Nothing was changed.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A1
content of A1 (a number, a label, or =formula)> =A1+1
A1 = #CYCLE; 0 formula cell(s) worked out again.
1 formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A2
content of A2 (a number, a label, or =formula)> =B1/C1
A2 = #DIV/0; 1 formula cell(s) worked out again.
1 formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A3
content of A3 (a number, a label, or =formula)> label
A3 = label; 1 formula cell(s) worked out again.
1 formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A4
content of A4 (a number, a label, or =formula)> =A3*2
A4 = #TEXT; 2 formula cell(s) worked out again.
1 formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A5
content of A5 (a number, a label, or =formula)> =SUM(A3:A4, B9)
A5 = #TEXT; 3 formula cell(s) worked out again.
1 formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A6
content of A6 (a number, a label, or =formula)> =1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1
Keep a cell to 60 characters.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> A6
content of A6 (a number, a label, or =formula)> =AVG(B1:B9)
A6 = #DIV/0; 4 formula cell(s) worked out again.
1 formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 2
cell to empty> A1
A1 is empty now; 4 formula cell(s) worked out again.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 3
              A          B          C          D          E          F
 1
 2       #DIV/0
 3   label
 4        #TEXT
 5        #TEXT
 6       #DIV/0
 7
 8
 9

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 4
              A          B          C          D          E          F
 1
 2   =B1/C1
 3   label
 4   =A3*2
 5   =SUM(A3:A~
 6   =AVG(B1:B~
 7
 8
 9

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 5
Worked out in this order: A2, A4, A6, A5.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 7
Bye.
```

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

Input:

```text
6
4
5
1
B3
1.25
1
B7
=D8/100
5
3
1
B7
0.1
3
7
```

Screen:

```text
== Mini spreadsheet ==
Cells A1 to F9 hold numbers, labels, or formulas such as =B2*C2 or =SUM(D2:D4).

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 6
A small order: three items, their totals, tax at 8% and an average.
              A          B          C          D          E          F
 1   Item       Price      Qty        Total
 2   Paper             4.5          3       13.5
 3   Pens              1.2         10         12
 4   Folders          2.75          4         11
 5
 6   Subtotal                               36.5
 7   Tax rate         0.08                  2.92
 8   Total                                 39.42
 9   Average                              ~12.17

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 4
              A          B          C          D          E          F
 1   Item       Price      Qty        Total
 2   Paper      4.5        3          =B2*C2
 3   Pens       1.2        10         =B3*C3
 4   Folders    2.75       4          =B4*C4
 5
 6   Subtotal                         =SUM(D2:D~
 7   Tax rate   0.08                  =D6*B7
 8   Total                            =D6+D7
 9   Average                          =AVG(D2:D~

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 5
Worked out in this order: D2, D3, D4, D6, D9, D7, D8.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> B3
content of B3 (a number, a label, or =formula)> 1.25
B3 = 1.25; 7 formula cell(s) worked out again.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> B7
content of B7 (a number, a label, or =formula)> =D8/100
B7 = #CYCLE; 5 formula cell(s) worked out again.
3 formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 5
Worked out in this order: D2, D3, D4, D6, D9.
In a cycle, or reading one: B7, D7, D8.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 3
              A          B          C          D          E          F
 1   Item       Price      Qty        Total
 2   Paper             4.5          3       13.5
 3   Pens             1.25         10       12.5
 4   Folders          2.75          4         11
 5
 6   Subtotal                                 37
 7   Tax rate       #CYCLE                #CYCLE
 8   Total                                #CYCLE
 9   Average                              ~12.33

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 1
cell> B7
content of B7 (a number, a label, or =formula)> 0.1
B7 = 0.1; 7 formula cell(s) worked out again.

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 3
              A          B          C          D          E          F
 1   Item       Price      Qty        Total
 2   Paper             4.5          3       13.5
 3   Pens             1.25         10       12.5
 4   Folders          2.75          4         11
 5
 6   Subtotal                                 37
 7   Tax rate          0.1                   3.7
 8   Total                                  40.7
 9   Average                              ~12.33

1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit
choice> 7
Bye.
```

## Modules

### main.eml

```eml
# P056 mini spreadsheet: six columns and nine rows of numbers, labels and
# formulas. Formulas use + - * / ( ), cells like B3 and SUM, MIN, MAX, AVG
# over ranges like A1:A5; every change recalculates the sheet in an order
# where each formula comes after the formulas it reads, and a cycle is
# found and named instead of looping forever.
import frac
import sheet

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 shown(v):
    # A value in at most 10 characters: numbers to two decimal places with
    # ~ in front when rounded, labels cut to fit, errors as they are.
    if v[0] == "e":
        return v[1]
    if v[0] == "t":
        if len(v[1]) > 10:
            return v[1][0:9] + "~"
        return v[1]
    frac.decimal(v[1], 2) => s
    if len(s) > 6 and s[0:6] == "about ":
        "~" + s[6:len(s)] => s
    if len(s) > 10:
        return "#WIDE"
    return s

def grid(cells, vals, formulas):
    "     " => head
    for c in [0:5]:
        "          " + "ABCDEF"[c] => h
        head + h[len(h) - 10:len(h)] + " " => head
    trim_right(head) ^0
    for r in [0:8]:
        " " + str(r + 1) + "   " => line
        for c in [0:5]:
            r * 6 + c => k
            if formulas:
                cells[k] => s
                if len(s) > 10:
                    s[0:9] + "~" => s
            else:
                shown(vals[k]) => s
            if formulas or vals[k][0] == "t":
                while len(s) < 10:
                    s + " " => s
            else:
                while len(s) < 10:
                    " " + s => s
            line + s + " " => line
        trim_right(line) ^0

def trim_right(s):
    len(s) => j
    while j > 0 and s[j - 1] == " ":
        j - 1 => j
    return s[0:j]

def ask_cell(prompt):
    while True:
        trim(input(prompt)) => answer
        if answer == "":
            return -1
        sheet.cell_index(answer) => k
        if k >= 0:
            return k
        "Type a cell from A1 to F9, like B3." ^0

def set_cell(state):
    ask_cell("cell> ") => k
    if k == -1:
        "Cancelled." ^0
        return
    trim(input("content of " + sheet.cell_name(k) + " (a number, a label, or =formula)> ")) => text
    if text == "":
        "Cancelled - use 2) to empty a cell." ^0
        return
    if len(text) > 60:
        "Keep a cell to 60 characters." ^0
        return
    if text[0] == "=":
        sheet.checked(text[1:len(text)]) => c
        if c[0] != "":
            ("Not a formula: " + c[0] + ". Nothing was changed.") ^0
            return
    text => state[0][k]
    update(state, k)

def update(state, k):
    sheet.recalculated(state[0]) => res
    res[0] => state[1]
    res[1] => state[2]
    sheet.cell_name(k) + " = " + shown(state[1][k]) => what
    if state[0][k] == "":
        sheet.cell_name(k) + " is empty now" => what
    (what + "; " + str(len(state[2])) + " formula cell(s) worked out again.") ^0
    0 => stuck
    for j in [0:53]:
        if state[0][j] != "" and state[0][j][0] == "=" and state[1][j][0] == "e" and state[1][j][1] == "#CYCLE":
            stuck + 1 => stuck
    if stuck > 0:
        (str(stuck) + " formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.") ^0

def clear_cell(state):
    ask_cell("cell to empty> ") => k
    if k == -1:
        "Cancelled." ^0
        return
    "" => state[0][k]
    update(state, k)

def order(state):
    if len(state[2]) == 0:
        "No formula has been worked out yet." ^0
    else:
        "" => s
        for k in state[2]:
            if s != "":
                s + ", " => s
            s + sheet.cell_name(k) => s
        ("Worked out in this order: " + s + ".") ^0
    "" => stuck
    for j in [0:53]:
        if state[0][j] != "" and state[0][j][0] == "=" and state[1][j][1] == "#CYCLE":
            if stuck != "":
                stuck + ", " => stuck
            stuck + sheet.cell_name(j) => stuck
    if stuck != "":
        ("In a cycle, or reading one: " + stuck + ".") ^0

def sample(state):
    [""] * 54 => cells
    [["A1", "Item"], ["B1", "Price"], ["C1", "Qty"], ["D1", "Total"],
     ["A2", "Paper"], ["B2", "4.5"], ["C2", "3"], ["D2", "=B2*C2"],
     ["A3", "Pens"], ["B3", "1.2"], ["C3", "10"], ["D3", "=B3*C3"],
     ["A4", "Folders"], ["B4", "2.75"], ["C4", "4"], ["D4", "=B4*C4"],
     ["A6", "Subtotal"], ["D6", "=SUM(D2:D4)"],
     ["A7", "Tax rate"], ["B7", "0.08"], ["D7", "=D6*B7"],
     ["A8", "Total"], ["D8", "=D6+D7"],
     ["A9", "Average"], ["D9", "=AVG(D2:D4)"]] => entries
    for e in entries:
        e[1] => cells[sheet.cell_index(e[0])]
    cells => state[0]
    sheet.recalculated(cells) => res
    res[0] => state[1]
    res[1] => state[2]
    "A small order: three items, their totals, tax at 8% and an average." ^0

"== Mini spreadsheet ==" ^0
"Cells A1 to F9 hold numbers, labels, or formulas such as =B2*C2 or =SUM(D2:D4)." ^0
# [cell texts, cell values, the order formulas were worked out]
[[""] * 54, [], []] => state
sheet.recalculated(state[0]) => first
first[0] => state[1]
True => running
while running:
    "" ^0
    "1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        set_cell(state)
    elif choice == "2":
        clear_cell(state)
    elif choice == "3":
        grid(state[0], state[1], False)
    elif choice == "4":
        grid(state[0], state[1], True)
    elif choice == "5":
        order(state)
    elif choice == "6":
        sample(state)
        grid(state[0], state[1], False)
    elif choice == "7":
        False => running
    else:
        "Pick a number from 1 to 7." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import frac
import sheet

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 shown(v):
    if v[0] == "e":
        return v[1]
    if v[0] == "t":
        if len(v[1]) > 10:
            return v[1][0:9] + "~"
        return v[1]
    s = frac.decimal(v[1], 2)
    if len(s) > 6 and s[0:6] == "about ":
        s = "~" + s[6:len(s)]
    if len(s) > 10:
        return "#WIDE"
    return s

def grid(cells, vals, formulas):
    head = "     "
    for c in range(0, 6):
        h = "          " + "ABCDEF"[c]
        head = head + h[len(h) - 10:len(h)] + " "
    print(trim_right(head))
    for r in range(0, 9):
        line = " " + str(r + 1) + "   "
        for c in range(0, 6):
            k = r * 6 + c
            if formulas:
                s = cells[k]
                if len(s) > 10:
                    s = s[0:9] + "~"
            else:
                s = shown(vals[k])
            if formulas or vals[k][0] == "t":
                while len(s) < 10:
                    s = s + " "
            else:
                while len(s) < 10:
                    s = " " + s
            line = line + s + " "
        print(trim_right(line))

def trim_right(s):
    j = len(s)
    while j > 0 and s[j - 1] == " ":
        j = j - 1
    return s[0:j]

def ask_cell(prompt):
    while True:
        answer = trim(input(prompt))
        if answer == "":
            return -1
        k = sheet.cell_index(answer)
        if k >= 0:
            return k
        print("Type a cell from A1 to F9, like B3.")

def set_cell(state):
    k = ask_cell("cell> ")
    if k == -1:
        print("Cancelled.")
        return
    text = trim(input("content of " + sheet.cell_name(k) + " (a number, a label, or =formula)> "))
    if text == "":
        print("Cancelled - use 2) to empty a cell.")
        return
    if len(text) > 60:
        print("Keep a cell to 60 characters.")
        return
    if text[0] == "=":
        c = sheet.checked(text[1:len(text)])
        if c[0] != "":
            print("Not a formula: " + c[0] + ". Nothing was changed.")
            return
    state[0][k] = text
    update(state, k)

def update(state, k):
    res = sheet.recalculated(state[0])
    state[1] = res[0]
    state[2] = res[1]
    what = sheet.cell_name(k) + " = " + shown(state[1][k])
    if state[0][k] == "":
        what = sheet.cell_name(k) + " is empty now"
    print(what + "; " + str(len(state[2])) + " formula cell(s) worked out again.")
    stuck = 0
    for j in range(0, 54):
        if state[0][j] != "" and state[0][j][0] == "=" and state[1][j][0] == "e" and state[1][j][1] == "#CYCLE":
            stuck = stuck + 1
    if stuck > 0:
        print(str(stuck) + " formula cell(s) are in a cycle, or read one - they show #CYCLE until it is broken.")

def clear_cell(state):
    k = ask_cell("cell to empty> ")
    if k == -1:
        print("Cancelled.")
        return
    state[0][k] = ""
    update(state, k)

def order(state):
    if len(state[2]) == 0:
        print("No formula has been worked out yet.")
    else:
        s = ""
        for k in state[2]:
            if s != "":
                s = s + ", "
            s = s + sheet.cell_name(k)
        print("Worked out in this order: " + s + ".")
    stuck = ""
    for j in range(0, 54):
        if state[0][j] != "" and state[0][j][0] == "=" and state[1][j][1] == "#CYCLE":
            if stuck != "":
                stuck = stuck + ", "
            stuck = stuck + sheet.cell_name(j)
    if stuck != "":
        print("In a cycle, or reading one: " + stuck + ".")

def sample(state):
    cells = [""] * 54
    entries = [["A1", "Item"], ["B1", "Price"], ["C1", "Qty"], ["D1", "Total"], ["A2", "Paper"], ["B2", "4.5"], ["C2", "3"], ["D2", "=B2*C2"], ["A3", "Pens"], ["B3", "1.2"], ["C3", "10"], ["D3", "=B3*C3"], ["A4", "Folders"], ["B4", "2.75"], ["C4", "4"], ["D4", "=B4*C4"], ["A6", "Subtotal"], ["D6", "=SUM(D2:D4)"], ["A7", "Tax rate"], ["B7", "0.08"], ["D7", "=D6*B7"], ["A8", "Total"], ["D8", "=D6+D7"], ["A9", "Average"], ["D9", "=AVG(D2:D4)"]]
    for e in entries:
        cells[sheet.cell_index(e[0])] = e[1]
    state[0] = cells
    res = sheet.recalculated(cells)
    state[1] = res[0]
    state[2] = res[1]
    print("A small order: three items, their totals, tax at 8% and an average.")

print("== Mini spreadsheet ==")
print("Cells A1 to F9 hold numbers, labels, or formulas such as =B2*C2 or =SUM(D2:D4).")
state = [[""] * 54, [], []]
first = sheet.recalculated(state[0])
state[1] = first[0]
running = True
while running:
    print("")
    print("1) set a cell  2) empty a cell  3) values  4) formulas  5) order  6) sample  7) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        set_cell(state)
    elif choice == "2":
        clear_cell(state)
    elif choice == "3":
        grid(state[0], state[1], False)
    elif choice == "4":
        grid(state[0], state[1], True)
    elif choice == "5":
        order(state)
    elif choice == "6":
        sample(state)
        grid(state[0], state[1], False)
    elif choice == "7":
        running = False
    else:
        print("Pick a number from 1 to 7.")
print("Bye.")
```

### sheet.eml

```eml
# P056 mini spreadsheet - formulas and recalculation. The sheet has columns
# A to F and rows 1 to 9; cell k (0 to 53) is column k % 6, row k / 6.
# A cell holds text as typed: "" (empty), a number, a formula starting
# with "=", or any other text (a label). A value is ["n", fraction],
# ["t", text] or ["e", error].
import frac

def cell_name(k):
    return "ABCDEF"[k % 6] + str(int(k / 6) + 1)

def cell_index(s):
    # "B3" or "b3" as 0 to 53, or -1.
    if len(s) != 2:
        return -1
    -1 => col
    for c in [0:5]:
        if s[0] == "ABCDEF"[c] or s[0] == "abcdef"[c]:
            c => col
    if col == -1 or not (s[1] >= "1" and s[1] <= "9"):
        return -1
    return (int(s[1]) - 1) * 6 + col

def tokens(f):
    # The formula text after "=" as tokens: ["num", fraction], ["cell", k],
    # ["fn", name], ["op", "+"] and so on; or [["bad", why]].
    [] => out
    0 => i
    while i < len(f):
        f[i] => c
        if c == " ":
            i + 1 => i
        elif c == "+" or c == "-" or c == "*" or c == "/" or c == "(" or c == ")" or c == ":" or c == ",":
            out + [["op", c]] => out
            i + 1 => i
        elif c >= "0" and c <= "9" or c == ".":
            i => j
            while j < len(f) and (f[j] >= "0" and f[j] <= "9" or f[j] == "."):
                j + 1 => j
            frac.parse(f[i:j]) => x
            if len(x) == 0:
                return [["bad", "the number " + f[i:j]]]
            out + [["num", x]] => out
            j => i
        elif (c >= "A" and c <= "Z") or (c >= "a" and c <= "z"):
            i => j
            while j < len(f) and ((f[j] >= "A" and f[j] <= "Z") or (f[j] >= "a" and f[j] <= "z") or (f[j] >= "0" and f[j] <= "9")):
                j + 1 => j
            f[i:j] => w
            cell_index(w) => k
            if k >= 0:
                out + [["cell", k]] => out
            else:
                "" => up
                for ch in w:
                    ch => u
                    for q in [0:25]:
                        if ch == "abcdefghijklmnopqrstuvwxyz"[q]:
                            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[q] => u
                    up + u => up
                if up == "SUM" or up == "MIN" or up == "MAX" or up == "AVG":
                    out + [["fn", up]] => out
                else:
                    return [["bad", w + ", which is not a cell from A1 to F9 or one of SUM, MIN, MAX, AVG"]]
            j => i
        else:
            return [["bad", "the character " + c]]
    return out

# The parser walks the tokens with a position kept in p[0]. Each step
# returns a value, given the values of all cells so far (vals); parsing
# alone (vals = []) only checks the shape and collects the cells read.

def peek(ts, p):
    if p[0] < len(ts):
        return ts[p[0]]
    return ["end", ""]

def is_op(t, c):
    return t[0] == "op" and t[1] == c

def arith(a, b, op):
    if a[0] == "e":
        return a
    if b[0] == "e":
        return b
    if a[0] == "t" or b[0] == "t":
        return ["e", "#TEXT"]
    if op == "+":
        return ["n", frac.plus(a[1], b[1])]
    if op == "-":
        return ["n", frac.minus(a[1], b[1])]
    if op == "*":
        return ["n", frac.times(a[1], b[1])]
    if frac.is_zero(b[1]):
        return ["e", "#DIV/0"]
    return ["n", frac.over(a[1], b[1])]

def read_cell(k, vals, refs):
    # refs is [the list of cells read so far], kept in a one-item list so
    # every call adds to the same one.
    refs[0] + [k] => refs[0]
    if len(vals) == 0:
        return ["n", [0, 1]]
    vals[k] => v
    if v[0] == "t" and v[1] == "":
        return ["n", [0, 1]]
    return v

def expr(ts, p, vals, refs):
    term(ts, p, vals, refs) => v
    while is_op(peek(ts, p), "+") or is_op(peek(ts, p), "-"):
        peek(ts, p)[1] => op
        p[0] + 1 => p[0]
        arith(v, term(ts, p, vals, refs), op) => v
    return v

def term(ts, p, vals, refs):
    factor(ts, p, vals, refs) => v
    while is_op(peek(ts, p), "*") or is_op(peek(ts, p), "/"):
        peek(ts, p)[1] => op
        p[0] + 1 => p[0]
        arith(v, factor(ts, p, vals, refs), op) => v
    return v

def factor(ts, p, vals, refs):
    peek(ts, p) => t
    if is_op(t, "-"):
        p[0] + 1 => p[0]
        return arith(["n", [0, 1]], factor(ts, p, vals, refs), "-")
    if t[0] == "num":
        p[0] + 1 => p[0]
        return ["n", t[1]]
    if t[0] == "cell":
        p[0] + 1 => p[0]
        return read_cell(t[1], vals, refs)
    if is_op(t, "("):
        p[0] + 1 => p[0]
        expr(ts, p, vals, refs) => v
        if not is_op(peek(ts, p), ")"):
            ["bad", "a ( without its )"] => p[1]
        else:
            p[0] + 1 => p[0]
        return v
    if t[0] == "fn":
        p[0] + 1 => p[0]
        return function(t[1], ts, p, vals, refs)
    ["bad", "something missing where a number, a cell or ( should be"] => p[1]
    return ["e", "#BAD"]

def function(fname, ts, p, vals, refs):
    # SUM, MIN, MAX or AVG of cells and ranges: (A1:A5) or (A1, B2, C1:C3).
    if not is_op(peek(ts, p), "("):
        ["bad", fname + " without ( after it"] => p[1]
        return ["e", "#BAD"]
    p[0] + 1 => p[0]
    [] => cells
    while True:
        peek(ts, p) => a
        if a[0] != "cell":
            ["bad", fname + "( with something other than cells and ranges in it, where " + fname + "(A1:A5) would do"] => p[1]
            return ["e", "#BAD"]
        p[0] + 1 => p[0]
        if is_op(peek(ts, p), ":"):
            p[0] + 1 => p[0]
            peek(ts, p) => b
            if b[0] != "cell":
                ["bad", "a range with no cell after its :"] => p[1]
                return ["e", "#BAD"]
            p[0] + 1 => p[0]
            a[1] % 6 => c1
            b[1] % 6 => c2
            int(a[1] / 6) => r1
            int(b[1] / 6) => r2
            if c2 < c1:
                c1 => t
                c2 => c1
                t => c2
            if r2 < r1:
                r1 => t
                r2 => r1
                t => r2
            for r in [r1:r2]:
                for c in [c1:c2]:
                    cells + [r * 6 + c] => cells
        else:
            cells + [a[1]] => cells
        if is_op(peek(ts, p), ","):
            p[0] + 1 => p[0]
        elif is_op(peek(ts, p), ")"):
            p[0] + 1 => p[0]
            break
        else:
            ["bad", fname + "( with no , between two cells or no ) at the end"] => p[1]
            return ["e", "#BAD"]
    # Empty cells and labels are left out, as spreadsheets do; an error in
    # any cell makes the whole result that error.
    [] => nums
    for k in cells:
        read_cell(k, vals, refs) => v
        if v[0] == "e":
            return v
        if v[0] == "n" and not (len(vals) > 0 and vals[k][0] == "t"):
            nums + [v[1]] => nums
    if len(vals) == 0:
        return ["n", [0, 1]]
    if fname == "SUM" or fname == "AVG":
        [0, 1] => s
        for x in nums:
            frac.plus(s, x) => s
        if fname == "SUM":
            return ["n", s]
        if len(nums) == 0:
            return ["e", "#DIV/0"]
        return ["n", frac.over(s, [len(nums), 1])]
    if len(nums) == 0:
        return ["n", [0, 1]]
    nums[0] => best
    for x in nums:
        if fname == "MIN" and frac.less(x, best):
            x => best
        if fname == "MAX" and frac.less(best, x):
            x => best
    return ["n", best]

def checked(text):
    # Parses a formula ("=" already taken off): [problem or "", the cells it
    # reads, the tokens].
    tokens(text) => ts
    if len(ts) > 0 and ts[0][0] == "bad":
        return ["it has " + ts[0][1], [], []]
    if len(ts) == 0:
        return ["it is empty", [], []]
    [0, ""] => p
    [[]] => refs
    expr(ts, p, [], refs)
    if p[1] != "":
        return ["it has " + p[1][1], [], []]
    if p[0] < len(ts):
        return ["something is left over after the formula ends", [], []]
    return ["", refs[0], ts]

def value_of(ts, vals):
    [0, ""] => p
    [[]] => refs
    return expr(ts, p, vals, refs)

def recalculated(cells):
    # Every cell's value, and the formula cells in the order they were
    # worked out. The order is Kahn's algorithm, as in the corpus case
    # topological-sort: a formula is ready once every formula it reads is
    # done; formulas never ready are caught in a cycle, or read one that is.
    [] => vals
    [] => formulas
    for k in [0:53]:
        cells[k] => s
        if s == "":
            vals + [["t", ""]] => vals
        elif s[0] == "=":
            vals + [["e", "#CYCLE"]] => vals
            formulas + [k] => formulas
        else:
            frac.parse(s) => x
            if len(x) > 0:
                vals + [["n", x]] => vals
            else:
                vals + [["t", s]] => vals
    # reads[k]: the formula cells formula k reads, once each
    {} => reads
    {} => waiting
    for k in formulas:
        checked(cells[k][1:len(cells[k])]) => c
        [] => mine
        for r in c[1]:
            if cells[r] != "" and cells[r][0] == "=":
                False => seen
                for m in mine:
                    if m == r:
                        True => seen
                if not seen:
                    mine + [r] => mine
        mine => reads[k]
        len(mine) => waiting[k]
    [] => ready
    for k in formulas:
        if waiting[k] == 0:
            ready + [k] => ready
    [] => order
    0 => head
    while head < len(ready):
        ready[head] => k
        head + 1 => head
        order + [k] => order
        tokens(cells[k][1:len(cells[k])]) => ts
        value_of(ts, vals) => vals[k]
        for j in formulas:
            for r in reads[j]:
                if r == k:
                    waiting[j] - 1 => waiting[j]
                    if waiting[j] == 0:
                        ready + [j] => ready
    return [vals, order]
```

Python projection of sheet.eml:

```python
import frac

def cell_name(k):
    return "ABCDEF"[k % 6] + str(int(k / 6) + 1)

def cell_index(s):
    if len(s) != 2:
        return -1
    col = -1
    for c in range(0, 6):
        if s[0] == "ABCDEF"[c] or s[0] == "abcdef"[c]:
            col = c
    if col == -1 or not (s[1] >= "1" and s[1] <= "9"):
        return -1
    return (int(s[1]) - 1) * 6 + col

def tokens(f):
    out = []
    i = 0
    while i < len(f):
        c = f[i]
        if c == " ":
            i = i + 1
        elif c == "+" or c == "-" or c == "*" or c == "/" or c == "(" or c == ")" or c == ":" or c == ",":
            out = out + [["op", c]]
            i = i + 1
        elif c >= "0" and c <= "9" or c == ".":
            j = i
            while j < len(f) and (f[j] >= "0" and f[j] <= "9" or f[j] == "."):
                j = j + 1
            x = frac.parse(f[i:j])
            if len(x) == 0:
                return [["bad", "the number " + f[i:j]]]
            out = out + [["num", x]]
            i = j
        elif c >= "A" and c <= "Z" or c >= "a" and c <= "z":
            j = i
            while j < len(f) and (f[j] >= "A" and f[j] <= "Z" or f[j] >= "a" and f[j] <= "z" or f[j] >= "0" and f[j] <= "9"):
                j = j + 1
            w = f[i:j]
            k = cell_index(w)
            if k >= 0:
                out = out + [["cell", k]]
            else:
                up = ""
                for ch in w:
                    u = ch
                    for q in range(0, 26):
                        if ch == "abcdefghijklmnopqrstuvwxyz"[q]:
                            u = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[q]
                    up = up + u
                if up == "SUM" or up == "MIN" or up == "MAX" or up == "AVG":
                    out = out + [["fn", up]]
                else:
                    return [["bad", w + ", which is not a cell from A1 to F9 or one of SUM, MIN, MAX, AVG"]]
            i = j
        else:
            return [["bad", "the character " + c]]
    return out

def peek(ts, p):
    if p[0] < len(ts):
        return ts[p[0]]
    return ["end", ""]

def is_op(t, c):
    return t[0] == "op" and t[1] == c

def arith(a, b, op):
    if a[0] == "e":
        return a
    if b[0] == "e":
        return b
    if a[0] == "t" or b[0] == "t":
        return ["e", "#TEXT"]
    if op == "+":
        return ["n", frac.plus(a[1], b[1])]
    if op == "-":
        return ["n", frac.minus(a[1], b[1])]
    if op == "*":
        return ["n", frac.times(a[1], b[1])]
    if frac.is_zero(b[1]):
        return ["e", "#DIV/0"]
    return ["n", frac.over(a[1], b[1])]

def read_cell(k, vals, refs):
    refs[0] = refs[0] + [k]
    if len(vals) == 0:
        return ["n", [0, 1]]
    v = vals[k]
    if v[0] == "t" and v[1] == "":
        return ["n", [0, 1]]
    return v

def expr(ts, p, vals, refs):
    v = term(ts, p, vals, refs)
    while is_op(peek(ts, p), "+") or is_op(peek(ts, p), "-"):
        op = peek(ts, p)[1]
        p[0] = p[0] + 1
        v = arith(v, term(ts, p, vals, refs), op)
    return v

def term(ts, p, vals, refs):
    v = factor(ts, p, vals, refs)
    while is_op(peek(ts, p), "*") or is_op(peek(ts, p), "/"):
        op = peek(ts, p)[1]
        p[0] = p[0] + 1
        v = arith(v, factor(ts, p, vals, refs), op)
    return v

def factor(ts, p, vals, refs):
    t = peek(ts, p)
    if is_op(t, "-"):
        p[0] = p[0] + 1
        return arith(["n", [0, 1]], factor(ts, p, vals, refs), "-")
    if t[0] == "num":
        p[0] = p[0] + 1
        return ["n", t[1]]
    if t[0] == "cell":
        p[0] = p[0] + 1
        return read_cell(t[1], vals, refs)
    if is_op(t, "("):
        p[0] = p[0] + 1
        v = expr(ts, p, vals, refs)
        if not is_op(peek(ts, p), ")"):
            p[1] = ["bad", "a ( without its )"]
        else:
            p[0] = p[0] + 1
        return v
    if t[0] == "fn":
        p[0] = p[0] + 1
        return function(t[1], ts, p, vals, refs)
    p[1] = ["bad", "something missing where a number, a cell or ( should be"]
    return ["e", "#BAD"]

def function(fname, ts, p, vals, refs):
    if not is_op(peek(ts, p), "("):
        p[1] = ["bad", fname + " without ( after it"]
        return ["e", "#BAD"]
    p[0] = p[0] + 1
    cells = []
    while True:
        a = peek(ts, p)
        if a[0] != "cell":
            p[1] = ["bad", fname + "( with something other than cells and ranges in it, where " + fname + "(A1:A5) would do"]
            return ["e", "#BAD"]
        p[0] = p[0] + 1
        if is_op(peek(ts, p), ":"):
            p[0] = p[0] + 1
            b = peek(ts, p)
            if b[0] != "cell":
                p[1] = ["bad", "a range with no cell after its :"]
                return ["e", "#BAD"]
            p[0] = p[0] + 1
            c1 = a[1] % 6
            c2 = b[1] % 6
            r1 = int(a[1] / 6)
            r2 = int(b[1] / 6)
            if c2 < c1:
                t = c1
                c1 = c2
                c2 = t
            if r2 < r1:
                t = r1
                r1 = r2
                r2 = t
            for r in range(r1, r2+1):
                for c in range(c1, c2+1):
                    cells = cells + [r * 6 + c]
        else:
            cells = cells + [a[1]]
        if is_op(peek(ts, p), ","):
            p[0] = p[0] + 1
        elif is_op(peek(ts, p), ")"):
            p[0] = p[0] + 1
            break
        else:
            p[1] = ["bad", fname + "( with no , between two cells or no ) at the end"]
            return ["e", "#BAD"]
    nums = []
    for k in cells:
        v = read_cell(k, vals, refs)
        if v[0] == "e":
            return v
        if v[0] == "n" and not (len(vals) > 0 and vals[k][0] == "t"):
            nums = nums + [v[1]]
    if len(vals) == 0:
        return ["n", [0, 1]]
    if fname == "SUM" or fname == "AVG":
        s = [0, 1]
        for x in nums:
            s = frac.plus(s, x)
        if fname == "SUM":
            return ["n", s]
        if len(nums) == 0:
            return ["e", "#DIV/0"]
        return ["n", frac.over(s, [len(nums), 1])]
    if len(nums) == 0:
        return ["n", [0, 1]]
    best = nums[0]
    for x in nums:
        if fname == "MIN" and frac.less(x, best):
            best = x
        if fname == "MAX" and frac.less(best, x):
            best = x
    return ["n", best]

def checked(text):
    ts = tokens(text)
    if len(ts) > 0 and ts[0][0] == "bad":
        return ["it has " + ts[0][1], [], []]
    if len(ts) == 0:
        return ["it is empty", [], []]
    p = [0, ""]
    refs = [[]]
    expr(ts, p, [], refs)
    if p[1] != "":
        return ["it has " + p[1][1], [], []]
    if p[0] < len(ts):
        return ["something is left over after the formula ends", [], []]
    return ["", refs[0], ts]

def value_of(ts, vals):
    p = [0, ""]
    refs = [[]]
    return expr(ts, p, vals, refs)

def recalculated(cells):
    vals = []
    formulas = []
    for k in range(0, 54):
        s = cells[k]
        if s == "":
            vals = vals + [["t", ""]]
        elif s[0] == "=":
            vals = vals + [["e", "#CYCLE"]]
            formulas = formulas + [k]
        else:
            x = frac.parse(s)
            if len(x) > 0:
                vals = vals + [["n", x]]
            else:
                vals = vals + [["t", s]]
    reads = {}
    waiting = {}
    for k in formulas:
        c = checked(cells[k][1:len(cells[k])])
        mine = []
        for r in c[1]:
            if cells[r] != "" and cells[r][0] == "=":
                seen = False
                for m in mine:
                    if m == r:
                        seen = True
                if not seen:
                    mine = mine + [r]
        reads[k] = mine
        waiting[k] = len(mine)
    ready = []
    for k in formulas:
        if waiting[k] == 0:
            ready = ready + [k]
    order = []
    head = 0
    while head < len(ready):
        k = ready[head]
        head = head + 1
        order = order + [k]
        ts = tokens(cells[k][1:len(cells[k])])
        vals[k] = value_of(ts, vals)
        for j in formulas:
            for r in reads[j]:
                if r == k:
                    waiting[j] = waiting[j] - 1
                    if waiting[j] == 0:
                        ready = ready + [j]
    return [vals, order]
```

### frac.eml

```eml
# P056 mini spreadsheet - exact fractions, from P007 (calculator) by way of
# P038 and P044.
# 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 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 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])
```

## README

# P056 - Mini spreadsheet

A small spreadsheet: columns A to F, rows 1 to 9. A cell holds a number, a
label, or a formula that starts with =. Formulas use + - * / and
parentheses, cells like B3, and SUM, MIN, MAX and AVG over cells and
ranges like D2:D4. Every change works the whole sheet out again, in an
order where each formula comes after the formulas it reads. A formula
caught in a cycle is found and marked instead of looping forever.

- `main.eml` - the menu, reading cells and contents, the value and formula
  views, and the sample
- `sheet.eml` - formulas: the tokens, a parser that also works the value
  out, and the recalculation order
- `frac.eml` - exact fractions, from P007 by way of P038 and P044

How each part works:

- Values are exact fractions, so 1.2 * 10 is exactly 12 and a third is a
  third. The value view shows two decimal places and marks a rounded value
  with ~.
- A formula is read by recursive descent: an expression is terms joined by
  + and -, a term is factors joined by * and /, and a factor is a number,
  a cell, a function, a minus sign before a factor, or an expression in
  parentheses. That is why =2+3*4 is 14. A formula that does not parse is
  refused with the reason, and nothing changes.
- An empty cell counts as 0 in arithmetic. Arithmetic on a label gives
  #TEXT, and dividing by zero gives #DIV/0; an error passes on to every
  formula that reads it. SUM, MIN, MAX and AVG skip empty cells and
  labels. AVG of nothing is #DIV/0, and MIN or MAX of nothing is 0.
- The order is Kahn's algorithm, as in the corpus case `topological-sort`.
  A formula is ready when every formula it reads is done; doing it may
  make others ready. Formulas never made ready are in a cycle, or read
  something that is, and they show #CYCLE until the cycle is broken. The
  order view lists both.

What is checked: menu choices 1 to 7; cells from A1 to F9 (either case);
contents up to 60 characters; formulas that parse - numbers, cells A1 to
F9, + - * / ( ), and SUM, MIN, MAX, AVG with cells and ranges separated by
commas. An empty answer cancels.

Sessions: `sessions/basic.in` loads a small order: three items, their
totals, a subtotal, tax at 8%, the total and the average.
- The views show values, formulas and the order they were worked out:
  D2, D3, D4, D6, D9, D7, D8.
- B3 changes to 1.25, and seven formulas are worked out again.
- B7 becomes =D8/100. D7 reads B7 and D8 reads D7, so the three make a
  cycle; the rest still work.
- B7 becomes 0.1, which breaks the cycle: tax 3.7 and total 40.7.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- cells G1 and A0, and an empty cell;
- empty contents;
- the broken formulas =1+ and =SUM(B1;
- =A1+1 in A1, a cycle of one;
- =B1/C1 on empty cells;
- a label, and a formula that multiplies it;
- a SUM over the label, that formula and an empty cell;
- 61 characters;
- AVG of an empty column;
- A1 emptied, then the values, the formulas and the order.

Built on the verified corpus case `topological-sort` (Kahn's algorithm,
which finds the cycle in what it leaves over).
