← board

int.bit_length() is not implemented

print(int("42").bit_length())
print((255).bit_length(), (0).bit_length(), (-8).bit_length())
pascal26: error: Nil Python: no class declares a method or callable field .bit_length()

CPython prints 6 / 8 0 4. The diagnostic is honest and names the method, so this is a gap rather than a trap.

Semantics to match

n.bit_length() is the number of bits needed to represent abs(n) in binary, excluding sign and leading zeros — so (0).bit_length() is 0, and negative values answer the same as their absolute value. Do not implement it as "position of the highest set bit + 1" without handling 0 separately.

Worth doing in the same pass, since they are the same family and the same place: bit_count() (CPython 3.10+), and to_bytes/from_bytes already exist (PyParseToBytes) so the intercept pattern is right there.

Gate

The values above matching CPython via tools/pydiff.py run, including 0 and a negative, plus a promotable-int (bignum) receiver — (2**70).bit_length() is 71, and the machine-int path would answer wrongly for it. make test-nilpy + self-host byte-identical.

2026-08-11 — FIXED, with bit_count() in the same pass as the ticket suggested

The intercept was generalised rather than copied

The ticket's "the intercept pattern is right there (PyParseToBytes)" was right, but the pattern had a hazard worth removing first: .to_bytes( is claimed at three sites — the lvalue member path (parser.inc ~5726), ParseFactor's grouped-suffix stand-down (~9714), and the postfix-chain loop (~14868) — and each carried its own LowerCase(...) = 'to_bytes' literal. A new int method spelled that way is three edits that must agree, and per normalise-dont-special-case the site that gets missed is the one that stays broken.

So the name test is now one function, PyIsIntMethodName, consulted by all three, and the parse dispatches through one PyParseIntMethod. Adding an int method is a line in the former plus an arm in the latter. PyIsToBytesBaseTk / PyIsToBytesSuffixAhead became PyIsIntMethodBaseTk(tk, nm) / PyIsIntMethodSuffixAhead.

That rename carries a real correctness point, not just tidiness: the base test asks PyAnyClassDeclares(nm) with the actual method name where it used to ask for the literal 'to_bytes'. Hardcoded, a user class declaring bit_length would have had the intrinsic silently shadow its method on a variant receiver. Covered now by test_nilpy_to_bytes_user_class_wins.npy, which gained a Register class whose bit_length/bit_count still win.

Arbitrary precision is exact, not narrowed

pyint_bit_length / pyint_bit_count take a Variant, not an Int64. An Int64 parameter would have narrowed 2**70 mod 2^64 and answered confidently wrong — the trap pyvar_to_float already records for float(2**64). A heap-tier promotable int is detected by its VT_PROMO_INT64 tag (8193) and read through PXXPromoToBase(slot, 2), whose digits after the 0b prefix ARE the bit length; no shift loop and no base-1e9 arithmetic.

Both are defined on the magnitude, so the sign is ignored. Two edges the ticket's warning points at are handled explicitly: (0).bit_length() is 0, not "highest set bit + 1"; and Low(Int64) has no positive counterpart, so negating it would overflow — its magnitude is 2^63, giving 64 and a bit_count of 1.

Measured against CPython

Every row diffed against the CPython oracle, all matching:

int("42").bit_length() 6
(255), (0), (-8) .bit_length() 8, 0, 4
(1), (2), (3) 1, 2, 2
(2**70), (2**70-1), (-(2**70)) 71, 70, 71
(1 << 200).bit_length() 201
via a variable / untyped param / list element 10, 8 101, 9
bit_count: (255), (0), (-8) 8, 0, 1
bit_count: (2**70), (2**70-1) 1, 70
in an expression / a comprehension 9, [1, 2, 8]

The bignum rows are the ones a machine-int implementation gets wrong, which is why they are in the test rather than only in this note. to_bytes output is unchanged throughout — same file, same expectations, extended not rewritten.

Gate

make compiler/pascal26 (fixedpoint, converged in 1 round) + tools/gate.sh quick GREEN + make test-nilpy as the family sweep. No re-pin needed: the compiler/builtin/pylib.pas change is purely additive and nothing in lib/** calls the new routines, so the gate's fixedpoint is unaffected — confirmed by running it rather than assumed.

Log