<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P160-upload-limits/ | updated: 2026-10-18 -->

# P160 Upload limits

One upload endpoint limited by files, by bytes and by both: 100 files lets in anywhere from under a megabyte to hundreds, and a byte limit bounds the most any upload gets in.

EML-P project `projects/upload-limits` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/upload-limits` runs it and `eml project verify projects/upload-limits` 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: cost-model-mismatch (https://efficientnewlanguage.org/cases/277-cost-model-mismatch/).

## Sessions

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

Input:

```text
0
x
3

3
0
3
1000
3
x
3
10

3
10
0
3
10
100000
3
10
x
3
150
8
4
```

Screen:

```text
== Upload limits ==
One upload endpoint limited by files, by bytes and by both, and what each lets in.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 0
Pick a number from 1 to 4.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> x
Pick a number from 1 to 4.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 
Cancelled.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 0
Type a whole number from 1 to 999.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 1000
Type a whole number from 1 to 999.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> x
Type a whole number from 1 to 999.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 10
about how many kB each (1 to 99999)> 
Cancelled.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 10
about how many kB each (1 to 99999)> 0
Type a whole number from 1 to 99999.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 10
about how many kB each (1 to 99999)> 100000
Type a whole number from 1 to 99999.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 10
about how many kB each (1 to 99999)> x
Type a whole number from 1 to 99999.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 150
about how many kB each (1 to 99999)> 8
  upload      100 files             20.0 MB               both
  yours       100 files, 0.9 MB     150 files, 1.4 MB     100 files, 0.9 MB
The file limit lets in 0.9 MB of it, the byte limit 1.4 MB, both 0.9 MB.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 4
Bye.
```

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

Input:

```text
1
2
3
300
9000
3
20
5
4
```

Screen:

```text
== Upload limits ==
One upload endpoint limited by files, by bytes and by both, and what each lets in.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 1
The uploads, as their files come in:
  thumbnails   150 files of about    8 kB,    1.4 MB in all
  receipts      60 files of about  120 kB,    8.6 MB in all
  photos       100 files of about 3500 kB,  420.0 MB in all
  scans         40 files of about 9000 kB,  432.0 MB in all
The limits: at most 100 files; at most 20.0 MB; or both.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 2
  upload      100 files             20.0 MB               both
  thumbnails  100 files, 0.9 MB     150 files, 1.4 MB     100 files, 0.9 MB
  receipts    60 files, 8.6 MB      60 files, 8.6 MB      60 files, 8.6 MB
  photos      100 files, 420.0 MB   5 files, 19.6 MB      5 files, 19.6 MB
  scans       40 files, 432.0 MB    2 files, 18.9 MB      2 files, 18.9 MB
The most let in: 432.0 MB under the file limit, 19.6 MB under the byte limit, 19.6 MB under both.
Under the 100-file limit an upload gets anywhere from 0.9 MB to 432.0 MB in: the limit counts what is easy to count, not what runs out.
The byte limit lets small uploads through whole and stops every upload at 20.0 MB: a limit bounds the most, it does not make uploads alike.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 300
about how many kB each (1 to 99999)> 9000
  upload      100 files             20.0 MB               both
  yours       100 files, 1080.0 MB  2 files, 18.9 MB      2 files, 18.9 MB
The file limit lets in 1080.0 MB of it, the byte limit 18.9 MB, both 18.9 MB.
Counted in files, the limit does not hold back the bytes that run out.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 3
how many files (1 to 999)> 20
about how many kB each (1 to 99999)> 5
  upload      100 files             20.0 MB               both
  yours       20 files, 0.1 MB      20 files, 0.1 MB      20 files, 0.1 MB
The file limit lets in 0.1 MB of it, the byte limit 0.1 MB, both 0.1 MB.

1) the uploads  2) the limits  3) an upload of your own  4) quit
choice> 4
Bye.
```

## Modules

### main.eml

```eml
# P160 upload limits: the uploads, each one under a limit on files, on
# bytes and on both, and an upload of your own.
import limits

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 mb(kb):
    # kilobytes as megabytes to one place, rounded half up
    int((kb + 50) / 100) => t
    t % 10 => d
    return str(int((t - d) / 10)) + "." + str(d) + " MB"

def total(sizes):
    0 => t
    for s in sizes:
        t + s => t
    return t

def show_uploads():
    "The uploads, as their files come in:" ^0
    for u in limits.uploads():
        limits.files(u) => fs
        ("  " + pad(u[0], 12) + lpad(str(len(fs)), 4) + " files of about " + lpad(str(u[2]), 4) + " kB, " + lpad(mb(total(fs)), 9) + " in all") ^0
    ("The limits: at most " + str(limits.count_limit()) + " files; at most " + mb(limits.byte_limit()) + "; or both.") ^0

def cell(r):
    return str(r[0]) + " files, " + mb(r[1])

def compare(us):
    ("  " + pad("upload", 12) + pad("100 files", 22) + pad("20.0 MB", 22) + "both") ^0
    [0, 0, 0] => most
    [0, 0, 0] => least
    0 => first
    for u in us:
        limits.files(u) => fs
        [limits.admit(fs, "count"), limits.admit(fs, "bytes"), limits.admit(fs, "both")] => rs
        ("  " + pad(u[0], 12) + pad(cell(rs[0]), 22) + pad(cell(rs[1]), 22) + cell(rs[2])) ^0
        0 => k
        while k < 3:
            if rs[k][1] > most[k]:
                rs[k][1] => most[k]
            if first == 0 or rs[k][1] < least[k]:
                rs[k][1] => least[k]
            k + 1 => k
        1 => first
    if len(us) == 1:
        ("The file limit lets in " + mb(most[0]) + " of it, the byte limit " + mb(most[1]) + ", both " + mb(most[2]) + ".") ^0
        if most[0] > limits.byte_limit():
            "Counted in files, the limit does not hold back the bytes that run out." ^0
    else:
        ("The most let in: " + mb(most[0]) + " under the file limit, " + mb(most[1]) + " under the byte limit, " + mb(most[2]) + " under both.") ^0
        if most[0] > limits.byte_limit():
            ("Under the 100-file limit an upload gets anywhere from " + mb(least[0]) + " to " + mb(most[0]) + " in: the limit counts what is easy to count, not what runs out.") ^0
        "The byte limit lets small uploads through whole and stops every upload at 20.0 MB: a limit bounds the most, it does not make uploads alike." ^0

def own():
    plain(input("how many files (1 to 999)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) > 3 or not digits(t) or int(t) < 1:
        "Type a whole number from 1 to 999." ^0
        return
    plain(input("about how many kB each (1 to 99999)> ")) => k
    if k == "":
        "Cancelled." ^0
        return
    if len(k) > 5 or not digits(k) or int(k) < 1:
        "Type a whole number from 1 to 99999." ^0
        return
    ["yours", int(t), int(k)] => u
    compare([u])

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

"== Upload limits ==" ^0
"One upload endpoint limited by files, by bytes and by both, and what each lets in." ^0
True => running
while running:
    "" ^0
    "1) the uploads  2) the limits  3) an upload of your own  4) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_uploads()
    elif choice == "2":
        compare(limits.uploads())
    elif choice == "3":
        own()
    elif choice == "4":
        False => running
    else:
        "Pick a number from 1 to 4." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import limits

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 mb(kb):
    t = int((kb + 50) / 100)
    d = t % 10
    return str(int((t - d) / 10)) + "." + str(d) + " MB"

def total(sizes):
    t = 0
    for s in sizes:
        t = t + s
    return t

def show_uploads():
    print("The uploads, as their files come in:")
    for u in limits.uploads():
        fs = limits.files(u)
        print("  " + pad(u[0], 12) + lpad(str(len(fs)), 4) + " files of about " + lpad(str(u[2]), 4) + " kB, " + lpad(mb(total(fs)), 9) + " in all")
    print("The limits: at most " + str(limits.count_limit()) + " files; at most " + mb(limits.byte_limit()) + "; or both.")

def cell(r):
    return str(r[0]) + " files, " + mb(r[1])

def compare(us):
    print("  " + pad("upload", 12) + pad("100 files", 22) + pad("20.0 MB", 22) + "both")
    most = [0, 0, 0]
    least = [0, 0, 0]
    first = 0
    for u in us:
        fs = limits.files(u)
        rs = [limits.admit(fs, "count"), limits.admit(fs, "bytes"), limits.admit(fs, "both")]
        print("  " + pad(u[0], 12) + pad(cell(rs[0]), 22) + pad(cell(rs[1]), 22) + cell(rs[2]))
        k = 0
        while k < 3:
            if rs[k][1] > most[k]:
                most[k] = rs[k][1]
            if first == 0 or rs[k][1] < least[k]:
                least[k] = rs[k][1]
            k = k + 1
        first = 1
    if len(us) == 1:
        print("The file limit lets in " + mb(most[0]) + " of it, the byte limit " + mb(most[1]) + ", both " + mb(most[2]) + ".")
        if most[0] > limits.byte_limit():
            print("Counted in files, the limit does not hold back the bytes that run out.")
    else:
        print("The most let in: " + mb(most[0]) + " under the file limit, " + mb(most[1]) + " under the byte limit, " + mb(most[2]) + " under both.")
        if most[0] > limits.byte_limit():
            print("Under the 100-file limit an upload gets anywhere from " + mb(least[0]) + " to " + mb(most[0]) + " in: the limit counts what is easy to count, not what runs out.")
        print("The byte limit lets small uploads through whole and stops every upload at 20.0 MB: a limit bounds the most, it does not make uploads alike.")

def own():
    t = plain(input("how many files (1 to 999)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) > 3 or not digits(t) or int(t) < 1:
        print("Type a whole number from 1 to 999.")
        return
    k = plain(input("about how many kB each (1 to 99999)> "))
    if k == "":
        print("Cancelled.")
        return
    if len(k) > 5 or not digits(k) or int(k) < 1:
        print("Type a whole number from 1 to 99999.")
        return
    u = ["yours", int(t), int(k)]
    compare([u])

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

print("== Upload limits ==")
print("One upload endpoint limited by files, by bytes and by both, and what each lets in.")
running = True
while running:
    print("")
    print("1) the uploads  2) the limits  3) an upload of your own  4) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_uploads()
    elif choice == "2":
        compare(limits.uploads())
    elif choice == "3":
        own()
    elif choice == "4":
        running = False
    else:
        print("Pick a number from 1 to 4.")
print("Bye.")
```

### limits.eml

```eml
# P160 upload limits - one upload endpoint, limited three ways. As in the
# corpus case cost-model-mismatch, a limit of 100 files is written in the
# unit that is easy to count, and what runs out is memory and bytes on the
# wire: 100 files is anywhere from a few kilobytes to many megabytes,
# depending on who is uploading. A limit's job is to bound the most it lets
# in, not to make every upload the same size, so each limit is judged by the
# largest upload it admits. Sizes are in kilobytes.

def count_limit():
    return 100

def byte_limit():
    # 20 megabytes, in kilobytes
    return 20000

def uploads():
    # [name, the sizes of its files in kilobytes, in the order they come]
    return [["thumbnails", 150, 8], ["receipts", 60, 120], ["photos", 100, 3500], ["scans", 40, 9000]]

def files(u):
    # the sizes of upload u's files: u[1] of them, sizes around u[2]
    [] => out
    0 => i
    while i < u[1]:
        out + [u[2] + int((i % 5) * u[2] / 10)] => out
        i + 1 => i
    return out

def admit(sizes, mode):
    # [files let in, kilobytes let in, files refused]
    0 => n
    0 => kb
    0 => refused
    for s in sizes:
        True => ok
        if mode == "count" or mode == "both":
            if n >= count_limit():
                False => ok
        if mode == "bytes" or mode == "both":
            if kb + s > byte_limit():
                False => ok
        if ok:
            n + 1 => n
            kb + s => kb
        else:
            refused + 1 => refused
    return [n, kb, refused]
```

Python projection of limits.eml:

```python
def count_limit():
    return 100

def byte_limit():
    return 20000

def uploads():
    return [["thumbnails", 150, 8], ["receipts", 60, 120], ["photos", 100, 3500], ["scans", 40, 9000]]

def files(u):
    out = []
    i = 0
    while i < u[1]:
        out = out + [u[2] + int(i % 5 * u[2] / 10)]
        i = i + 1
    return out

def admit(sizes, mode):
    n = 0
    kb = 0
    refused = 0
    for s in sizes:
        ok = True
        if mode == "count" or mode == "both":
            if n >= count_limit():
                ok = False
        if mode == "bytes" or mode == "both":
            if kb + s > byte_limit():
                ok = False
        if ok:
            n = n + 1
            kb = kb + s
        else:
            refused = refused + 1
    return [n, kb, refused]
```

## README

# P160 - Upload limits

One upload endpoint limited three ways - by files, by bytes and by both -
and the most each lets in.

- `main.eml` - the menu, the uploads, the uploads under each limit, and an
  upload of your own
- `limits.eml` - the two limits, the uploads and the sizes of their files,
  and what a limit lets in

How each part works:

- Four uploads: 150 thumbnails of about 8 kB, 60 receipts of about 120 kB,
  100 photos of about 3.5 MB and 40 scans of about 9 MB.
- As in the corpus case `cost-model-mismatch`, a limit of 100 files is
  written in the unit that is easy to count, and what runs out is memory
  and bytes on the wire. Under it an upload gets anywhere from 0.9 MB to
  432.0 MB in.
- A limit is judged by the most it lets in, not by making uploads alike: at
  20.0 MB the byte limit lets the small uploads through whole and stops
  every upload at 20.0 MB, 19.6 MB at most here. Both limits together never
  let in more than either alone.

What is checked: menu choices 1 to 4; for an upload of your own, 1 to 999
files of about 1 to 99999 kB each. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the uploads and the limits;
- 300 files of about 9000 kB, and 20 files of about 5 kB.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- files: a cancel, 0, 1000 and x; then 10 files with a cancel, 0, 100000
  and x for the size;
- 150 files of about 8 kB, where the file limit lets in less than the byte
  limit.

Built on the verified corpus case `cost-model-mismatch` (the same workloads
through a count limit, a size limit and both, judged by the maximum each
admits).
