<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P140-parts-rollup/ | updated: 2026-10-17 -->

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

EML-P project `projects/parts-rollup` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/parts-rollup` runs it and `eml project verify projects/parts-rollup` 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: rollup-double-counts-shared-parent (https://efficientnewlanguage.org/cases/304-rollup-double-counts-shared-parent/).

## Sessions

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

Input:

```text
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
```

Screen:

```text
== 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.
```

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

Input:

```text
1
2
3
4
wheel
spoke
36
2
3
5
```

Screen:

```text
== 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.
```

## Modules

### main.eml

```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 of main.eml:

```python
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 of bom.eml:

```python
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
```

## README

# P140 - Parts rollup

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