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:
bytes(n)was refused wherebytearray(n)was accepted. n zero bytes; CPython's oldest bytes constructor and how a program allocates a sized buffer.sorted()over bytes raisedTypeError: expected a number, got objectwheresorted()over str and over int both worked.pyvar_gthad a lexicographic arm for twoTPyLists (list/tuple/set are one class here) and none forTPyBytes, so bytes fell through to the numeric path. Filed and fixed asbug-n-sorted-over-bytes-raises-where-sorted-over-str-works. The comparison is UNSIGNED —b"\xff" > b"\x01"— and the test asserts the high-bit pairs, because a signed reading still returns a sorted-LOOKING list and only a value assertion over such a pair separates the two.
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
- 2026-09-09 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit d1efd1dee.