← board

Minimal

class A:
    def steps(self):
        return [lambda g, f, t, p: len(b"abcd")]
A().steps()[0](1, 2, 3, 4)

pxx    TypeError: expected a number, got str
CPython 4

The same body OUTSIDE a closure is correct, because a compiled bytes literal never goes near pyeval.

How it presented

lekkerzeilen app.py:1752, len(indices or b"") inside lambda g, f, t, p: self._reserve_mesh(stage, name, layout, len(vertices), len(indices or b"")). The literal is EMPTY, so the message named an empty string and read like a missing argument rather than like a literal. A probe on pyvar_to_int's refusal arm printing the offending value -- PROBE to_int got str: [] -- is what turned it from "somewhere in that lambda" into three lines.

The divergence this accepts, deliberately

bytes("abc") without an encoding is a TypeError in CPython. It is accepted here, because by the time the interpreter sees it the literal and the call have already collapsed into one spelling, so refusing would refuse the literal -- and the literal is the shape real code writes. NilPy is upward compatible with CPython by design; recorded in nilpy-semantics-divergences.md.

The alternative, not taken

The other repair is upstream: keep the bytes literal as a bytes-literal TOKEN in the closure snapshot, so it reaches ParsePrimary's existing arm and never becomes a call. That is more faithful and it is strictly more work, and the constructor still needs a string arm for the call spelling to mean anything. Worth doing if the snapshot's re-render is ever revisited for another reason.

Guard

test_nilpy_a_bytes_literal_inside_an_interpreted_closure. The COMPILED read is the positive control; the empty literal is not last, because an instrument that only saw the empty spelling would read the failure as an arity problem; and bytes(n) is a row so the fix cannot swallow the int arm. Reverted, the fixture dies at the second line with the original message.

Log