Project P140

Parts rollup

A bike's bill of materials with a part shared by three parents: parts and cost per bike multiplied down every path, rightly, and a tooling cost paid once that the same tree rollup counts once per path.

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

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

A bike's bill of materials: each part made of other parts, so many of each, with a cost per unit and a tooling cost paid once per kind of part.

  • main.eml - the menu, the parts list, the parts one bike needs, the costs rolled up two ways, and changing a quantity
  • bom.eml - the parts, the order they are reached, units per bike, cost per bike, tooling rolled up as a tree and once per part, the paths to each part, and a changed quantity

How each part works:

  • The bearing kit goes into the frame, each wheel and the drivetrain, so the list is a graph, not a tree: three paths reach it.
  • How many of each part a bike needs multiplies the quantities down every path: 4 bearing kits, 64 spokes. That is right, because each path is parts that really go into the bike, and so is the cost per bike, 398.
  • As in the corpus case rollup-double-counts-shared-parent, the same rollup is wrong for a cost paid once. Rolled up as a tree, the tooling is 1750; once for each kind of part it is 1150. The 600 too much is the bearing kit's tooling, 300, counted on two extra paths: the overstatement is always each shared part's tooling times its extra paths.

What is checked: menu choices 1 to 5; a part that has parts, one of its own parts, and a quantity from 1 to 99. An empty answer cancels.

Sessions: sessions/basic.in gives: - the parts list, the parts per bike and the costs; - 36 spokes a wheel, and the parts and costs again.

sessions/bad-input.in gives: - menu choices 0 and x; - changes: a cancel, spoke (no parts of its own) and cart, then wheel with a cancel and chain, and wheel and spoke with a cancel, 0, 100 and x; - 3 wheels a bike, and the parts and costs.

Built on the verified corpus case rollup-double-counts-shared-parent (a recursive rollup over a hierarchy that is not a tree).

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
== Parts rollup ==
A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 0
Pick a number from 1 to 5.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> x
Pick a number from 1 to 5.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> 
Cancelled.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> spoke
Type a part that is made of other parts: bike, frame, wheel or drivetrain.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> cart
Type a part that is made of other parts: bike, frame, wheel or drivetrain.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> 
Cancelled.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> chain
chain is not one of the parts of wheel.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> 
Cancelled.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> 0
Type a whole number from 1 to 99.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> 100
Type a whole number from 1 to 99.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> x
Type a whole number from 1 to 99.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> bike
which of its parts> wheel
how many (1 to 99)> 3
A bike now takes 3 x wheel.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 2
Parts for one bike, multiplied down every path:
  frame          1
  wheel          3
  drivetrain     1
  bearing kit    5
  rim            3
  spoke         96
  chain          1
  crank          1
Multiplying down every path is right here: each path is parts that really go into the bike.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 3
Cost of one bike, each part's own cost times how many: 478
Tooling, paid once for each kind of part:
  part           tooling   paths   counted by the tree
  frame              500       1                   500
  bearing kit        300       3                   900
  rim                200       1                   200
  crank              150       1                   150
  rolled up as a tree: 1750; once for each part: 1150
The tree counts the tooling of a shared part once for every path to it: 600 too much. The same rollup is right for the cost per bike and wrong for a cost paid once.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 5
Bye.
What was typed (37 lines)
0
x
4

4
spoke
4
cart
4
wheel

4
wheel
chain
4
wheel
spoke

4
wheel
spoke
0
4
wheel
spoke
100
4
wheel
spoke
x
4
bike
wheel
3
2
3
5

basic

interpreter: byte-equal
== Parts rollup ==
A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 1
The parts list, each part under every part that uses it:
  1 x bike
    1 x frame
      1 x bearing kit
    2 x wheel
      1 x rim
      32 x spoke
      1 x bearing kit
    1 x drivetrain
      1 x chain
      1 x crank
      1 x bearing kit
  bearing kit is used under 3 parts: the list is a graph, not a tree.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 2
Parts for one bike, multiplied down every path:
  frame          1
  wheel          2
  drivetrain     1
  bearing kit    4
  rim            2
  spoke         64
  chain          1
  crank          1
Multiplying down every path is right here: each path is parts that really go into the bike.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 3
Cost of one bike, each part's own cost times how many: 398
Tooling, paid once for each kind of part:
  part           tooling   paths   counted by the tree
  frame              500       1                   500
  bearing kit        300       3                   900
  rim                200       1                   200
  crank              150       1                   150
  rolled up as a tree: 1750; once for each part: 1150
The tree counts the tooling of a shared part once for every path to it: 600 too much. The same rollup is right for the cost per bike and wrong for a cost paid once.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 4
part (one that has parts)> wheel
which of its parts> spoke
how many (1 to 99)> 36
A wheel now takes 36 x spoke.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 2
Parts for one bike, multiplied down every path:
  frame          1
  wheel          2
  drivetrain     1
  bearing kit    4
  rim            2
  spoke         72
  chain          1
  crank          1
Multiplying down every path is right here: each path is parts that really go into the bike.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 3
Cost of one bike, each part's own cost times how many: 406
Tooling, paid once for each kind of part:
  part           tooling   paths   counted by the tree
  frame              500       1                   500
  bearing kit        300       3                   900
  rim                200       1                   200
  crank              150       1                   150
  rolled up as a tree: 1750; once for each part: 1150
The tree counts the tooling of a shared part once for every path to it: 600 too much. The same rollup is right for the cost per bike and wrong for a cost paid once.

1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit
choice> 5
Bye.
What was typed (10 lines)
1
2
3
4
wheel
spoke
36
2
3
5

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
# P140 parts rollup: the parts list as a tree, how many of each part a bike
# needs, the bike's costs rolled up two ways, and changing a quantity.
import bom

def plain(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 pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def tree(b, part, q, depth):
    ("  " + "  " * depth + str(q) + " x " + part) ^0
    for c in b[part][2]:
        tree(b, c[0], c[1], depth + 1)

def show(b):
    "The parts list, each part under every part that uses it:" ^0
    tree(b, "bike", 1, 0)
    bom.paths(b, "bike", {}) => ps
    for p in bom.names(b):
        if ps[p] > 1:
            ("  " + p + " is used under " + str(ps[p]) + " parts: the list is a graph, not a tree.") ^0

def per_bike(b):
    bom.units(b, "bike", 1, {}) => u
    "Parts for one bike, multiplied down every path:" ^0
    for p in bom.names(b):
        if p != "bike":
            ("  " + pad(p, 12) + lpad(str(u[p]), 4)) ^0
    "Multiplying down every path is right here: each path is parts that really go into the bike." ^0

def costs(b):
    bom.unit_cost(b, "bike") => c
    ("Cost of one bike, each part's own cost times how many: " + str(c)) ^0
    bom.tooling_by_path(b, "bike") => t1
    bom.tooling_once(b) => t2
    bom.paths(b, "bike", {}) => ps
    "Tooling, paid once for each kind of part:" ^0
    "  part           tooling   paths   counted by the tree" ^0
    for p in bom.names(b):
        if b[p][1] > 0:
            ("  " + pad(p, 13) + lpad(str(b[p][1]), 9) + lpad(str(ps[p]), 8) + lpad(str(b[p][1] * ps[p]), 22)) ^0
    ("  rolled up as a tree: " + str(t1) + "; once for each part: " + str(t2)) ^0
    if t1 > t2:
        ("The tree counts the tooling of a shared part once for every path to it: " + str(t1 - t2) + " too much. The same rollup is right for the cost per bike and wrong for a cost paid once.") ^0

def find(b, name):
    for p in b:
        if p == name:
            return True
    return False

def change(b):
    plain(input("part (one that has parts)> ")) => parent
    if parent == "":
        "Cancelled." ^0
        return b
    if not find(b, parent) or len(b[parent][2]) == 0:
        "Type a part that is made of other parts: bike, frame, wheel or drivetrain." ^0
        return b
    plain(input("which of its parts> ")) => child
    if child == "":
        "Cancelled." ^0
        return b
    False => ok
    for c in b[parent][2]:
        if c[0] == child:
            True => ok
    if not ok:
        (child + " is not one of the parts of " + parent + ".") ^0
        return b
    plain(input("how many (1 to 99)> ")) => t
    if t == "":
        "Cancelled." ^0
        return b
    if len(t) > 2:
        "Type a whole number from 1 to 99." ^0
        return b
    for c in t:
        if c < "0" or c > "9":
            "Type a whole number from 1 to 99." ^0
            return b
    if int(t) < 1:
        "Type a whole number from 1 to 99." ^0
        return b
    ("A " + parent + " now takes " + t + " x " + child + ".") ^0
    return bom.set_quantity(b, parent, child, int(t))

bom.parts() => b

"== Parts rollup ==" ^0
"A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once." ^0
True => running
while running:
    "" ^0
    "1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show(b)
    elif choice == "2":
        per_bike(b)
    elif choice == "3":
        costs(b)
    elif choice == "4":
        change(b) => b
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import bom

def plain(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 pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def tree(b, part, q, depth):
    print("  " + "  " * depth + str(q) + " x " + part)
    for c in b[part][2]:
        tree(b, c[0], c[1], depth + 1)

def show(b):
    print("The parts list, each part under every part that uses it:")
    tree(b, "bike", 1, 0)
    ps = bom.paths(b, "bike", {})
    for p in bom.names(b):
        if ps[p] > 1:
            print("  " + p + " is used under " + str(ps[p]) + " parts: the list is a graph, not a tree.")

def per_bike(b):
    u = bom.units(b, "bike", 1, {})
    print("Parts for one bike, multiplied down every path:")
    for p in bom.names(b):
        if p != "bike":
            print("  " + pad(p, 12) + lpad(str(u[p]), 4))
    print("Multiplying down every path is right here: each path is parts that really go into the bike.")

def costs(b):
    c = bom.unit_cost(b, "bike")
    print("Cost of one bike, each part's own cost times how many: " + str(c))
    t1 = bom.tooling_by_path(b, "bike")
    t2 = bom.tooling_once(b)
    ps = bom.paths(b, "bike", {})
    print("Tooling, paid once for each kind of part:")
    print("  part           tooling   paths   counted by the tree")
    for p in bom.names(b):
        if b[p][1] > 0:
            print("  " + pad(p, 13) + lpad(str(b[p][1]), 9) + lpad(str(ps[p]), 8) + lpad(str(b[p][1] * ps[p]), 22))
    print("  rolled up as a tree: " + str(t1) + "; once for each part: " + str(t2))
    if t1 > t2:
        print("The tree counts the tooling of a shared part once for every path to it: " + str(t1 - t2) + " too much. The same rollup is right for the cost per bike and wrong for a cost paid once.")

def find(b, name):
    for p in b:
        if p == name:
            return True
    return False

def change(b):
    parent = plain(input("part (one that has parts)> "))
    if parent == "":
        print("Cancelled.")
        return b
    if not find(b, parent) or len(b[parent][2]) == 0:
        print("Type a part that is made of other parts: bike, frame, wheel or drivetrain.")
        return b
    child = plain(input("which of its parts> "))
    if child == "":
        print("Cancelled.")
        return b
    ok = False
    for c in b[parent][2]:
        if c[0] == child:
            ok = True
    if not ok:
        print(child + " is not one of the parts of " + parent + ".")
        return b
    t = plain(input("how many (1 to 99)> "))
    if t == "":
        print("Cancelled.")
        return b
    if len(t) > 2:
        print("Type a whole number from 1 to 99.")
        return b
    for c in t:
        if c < "0" or c > "9":
            print("Type a whole number from 1 to 99.")
            return b
    if int(t) < 1:
        print("Type a whole number from 1 to 99.")
        return b
    print("A " + parent + " now takes " + t + " x " + child + ".")
    return bom.set_quantity(b, parent, child, int(t))

b = bom.parts()
print("== Parts rollup ==")
print("A bike's bill of materials, rolled up into parts, a cost per bike, and the tooling paid once.")
running = True
while running:
    print("")
    print("1) parts list  2) parts per bike  3) costs  4) change a quantity  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show(b)
    elif choice == "2":
        per_bike(b)
    elif choice == "3":
        costs(b)
    elif choice == "4":
        b = change(b)
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

bom.eml

eml
# P140 parts rollup - a bill of materials: each part made of other parts,
# so many of each. A part used under several parents makes the list a graph,
# not a tree. Counting how many of each part a bike needs, and its cost per
# bike, multiplies down every path, and that is right: each path is real
# parts. As in the corpus case rollup-double-counts-shared-parent, a cost
# paid once per part - the tooling to make it - is counted once for every
# path that reaches the part when rolled up the same way.

def parts():
    # part: [own cost per unit (labour, or the part itself), tooling paid
    #  once, [[child, quantity], ...]]
    return {"bike": [40, 0, [["frame", 1], ["wheel", 2], ["drivetrain", 1]]], "frame": [120, 500, [["bearing kit", 1]]], "wheel": [15, 0, [["rim", 1], ["spoke", 32], ["bearing kit", 1]]], "drivetrain": [20, 0, [["chain", 1], ["crank", 1], ["bearing kit", 1]]], "rim": [25, 200, []], "spoke": [1, 0, []], "chain": [12, 0, []], "crank": [30, 150, []], "bearing kit": [8, 300, []]}

def names(bom):
    # parts in the order they are first reached from the bike
    ["bike"] => out
    0 => k
    while k < len(out):
        for c in bom[out[k]][2]:
            False => seen
            for x in out:
                if x == c[0]:
                    True => seen
            if not seen:
                out + [c[0]] => out
        k + 1 => k
    return out

def units(bom, part, times, acc):
    # how many of each part one of part needs, times times, added into acc
    if part in acc:
        acc[part] + times => acc[part]
    else:
        times => acc[part]
    for c in bom[part][2]:
        units(bom, c[0], times * c[1], acc)
    return acc

def unit_cost(bom, part):
    # the cost of one: its own, and each child's times how many
    bom[part][0] => c
    for ch in bom[part][2]:
        c + ch[1] * unit_cost(bom, ch[0]) => c
    return c

def tooling_by_path(bom, part):
    # the tooling rolled up as a tree: own tooling, plus each child's rolled
    # up tooling (once per child, not per unit)
    bom[part][1] => t
    for ch in bom[part][2]:
        t + tooling_by_path(bom, ch[0]) => t
    return t

def tooling_once(bom):
    0 => t
    for p in names(bom):
        t + bom[p][1] => t
    return t

def paths(bom, part, acc):
    # how many paths from the bike reach each part
    if part in acc:
        acc[part] + 1 => acc[part]
    else:
        1 => acc[part]
    for ch in bom[part][2]:
        paths(bom, ch[0], acc)
    return acc

def set_quantity(bom, parent, child, q):
    {} => out
    for p in bom:
        [] => kids
        for c in bom[p][2]:
            if p == parent and c[0] == child:
                kids + [[c[0], q]] => kids
            else:
                kids + [[c[0], c[1]]] => kids
        [bom[p][0], bom[p][1], kids] => out[p]
    return out
Python projection (bom.py)
def parts():
    return {"bike": [40, 0, [["frame", 1], ["wheel", 2], ["drivetrain", 1]]], "frame": [120, 500, [["bearing kit", 1]]], "wheel": [15, 0, [["rim", 1], ["spoke", 32], ["bearing kit", 1]]], "drivetrain": [20, 0, [["chain", 1], ["crank", 1], ["bearing kit", 1]]], "rim": [25, 200, []], "spoke": [1, 0, []], "chain": [12, 0, []], "crank": [30, 150, []], "bearing kit": [8, 300, []]}

def names(bom):
    out = ["bike"]
    k = 0
    while k < len(out):
        for c in bom[out[k]][2]:
            seen = False
            for x in out:
                if x == c[0]:
                    seen = True
            if not seen:
                out = out + [c[0]]
        k = k + 1
    return out

def units(bom, part, times, acc):
    if part in acc:
        acc[part] = acc[part] + times
    else:
        acc[part] = times
    for c in bom[part][2]:
        units(bom, c[0], times * c[1], acc)
    return acc

def unit_cost(bom, part):
    c = bom[part][0]
    for ch in bom[part][2]:
        c = c + ch[1] * unit_cost(bom, ch[0])
    return c

def tooling_by_path(bom, part):
    t = bom[part][1]
    for ch in bom[part][2]:
        t = t + tooling_by_path(bom, ch[0])
    return t

def tooling_once(bom):
    t = 0
    for p in names(bom):
        t = t + bom[p][1]
    return t

def paths(bom, part, acc):
    if part in acc:
        acc[part] = acc[part] + 1
    else:
        acc[part] = 1
    for ch in bom[part][2]:
        paths(bom, ch[0], acc)
    return acc

def set_quantity(bom, parent, child, q):
    out = {}
    for p in bom:
        kids = []
        for c in bom[p][2]:
            if p == parent and c[0] == child:
                kids = kids + [[c[0], q]]
            else:
                kids = kids + [[c[0], c[1]]]
        out[p] = [bom[p][0], bom[p][1], kids]
    return out

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.