← board

What is known

$ ./compiler/pascal26 lekkerzeilen/text.py out
pascal26:144: error: no overload of join matches these arguments

lekkerzeilen's runtime package uses 21 generator expressions and 20 list comprehensions, so sep.join(<genexp>) is the shape to try first.

What is NOT known, deliberately

The reduction. A first-error census stops at the first failure per module, so this is where text.py stops today and nothing establishes it is the only problem in that file. Reduce before fixing — the cause could be the argument being a generator rather than a sequence, an element type, or the overload resolution itself, and those are three different fixes.

RESOLVED 2026-09-09 (frankB) — and the ticket's own name is wrong: it is BYTES, not str

Reduced first, as the ticket asked. The failing call at lekkerzeilen/text.py:144 is

b"".join(reversed(rows))

a bytes separator, not a str one. str.join was never the problem. A census of the argument shapes settles which half was missing:

call before
"".join(reversed(x)) works
"".join(gen), tuple, map, sorted works
b"".join(list(reversed(x))) works
b"".join(reversed(x)) compile error

So every iterable shape reached str.join, and bytes.join had only the TPyList overload — the one thing missing was MATERIALISING. reversed() and sorted() return a TPyIter; a generator and a tuple already arrived as a TPyList.

Fixed by adding function join(parts: TPyIter): TPyBytes to TPyBytes, which materialises and delegates to the list arm rather than carrying a second copy of the walk. It needed the unit's first forward class declaration (TPyIter = class;) because TPyIter is declared 200 lines below TPyBytes; the alternative was moving a 200-line class and making the diff unreadable for one signature.

Two spellings of one concept, and the one nobody extended is the one that stayed broken — devdocs/dev/normalise-dont-special-case.md, which is why the fix was followed by a grep for the siblings. That grep found two more, both fixed here:

test/test_nilpy_bytes_join_and_bytes_n.npy covers all three plus the str twins, .expected generated by CPython, with a Makefile row.

text.py compiles — the one module in this group's three that the fix actually moved (8 of 23). The array and deque fixes each cleared their module's first wall and found a deeper one behind it.

AND bytes(n) REGRESSED A VARIANT RECEIVER — caught by the tier, fixed here

Recorded because the fix is one line and the mechanism is not obvious.

Adding bytes(n: Integer) for an unrelated shape (bytes(count * 2)) moved a call that was ALREADY resolving. A dynamically-typed receiver —

for y in [[3, 1, 2]]:
    print(len(bytes(y)))          # 3

— had been binding to the bytes(b: TPyBytes) overload and being rescued by that body's runtime is TPyList check, which pylib documents as "belt and braces" and believed unreachable. With an Integer overload present the Variant matched the INTEGER instead, and the call aborted at run time with expected a number, got object. Nothing was refused at compile time.

An overload set is a resolution TABLE, so adding a row can move an argument that was already resolving. A new overload is never purely additive once a Variant can reach it.

What caught it: test_nilpy_builtin_over_variant_receiver, which exists for this exact class and whose own header says both payload kinds are swept because testing only the static one shows nothing. It was right: the static-list spelling (bytes([3,1,2])) and the genuine-int spelling (bytes(4)) both still passed, and my own four new test files all still passed. Only the variant row moved.

The fix is the VARIANT arm that max/min/len/tuple/sum already carry, so the dynamic case is resolved explicitly instead of by accident. It dispatches on the runtime tag rather than delegating to pylist_v, which those others use: pylist_v SPREADS a str into its characters, and bytes(s) with no encoding is a TypeError in CPython — asserted two lines further down in that same test.

Pre-existing and NOT fixed here: bytes(s, "utf8"), the two-argument encoding form, has no overload at all. s.encode("utf8") works, so the gap is narrow.

Log