← board

PyException leaks through __name__ and repr()

Reproduce — no imports, nothing synthetic

try:
    raise Exception("plain")
except Exception as e:
    print("class name:", type(e).__name__)
    print("repr:", repr(e))
    print("str:", str(e))
pxx CPython
type(e).__name__ PyException Exception
repr(e) PyException('plain') Exception('plain')
str(e) plain plain

This is the important part: it is not the sysutils-collision case. That one is synthetic and was closed as such ([[decide-merge-variant-c-with-bare-name-collision]]). This reproduces in an ordinary Python program that imports nothing at all, so it is squarely inside NilPy's upward-compatibility rule — code that works on CPython must work on NilPy. A program that branches on type(e).__name__ == 'Exception', or that prints a repr into a log or a test expectation, sees the mangling.

Note the derived classes are FINE — raise ValueError("boom") reports ValueError and ValueError('boom') correctly. It is only the base class, i.e. exactly the one the lexer rewrites.

Second, smaller gap found alongside

$ ... print("base name:", Exception.__name__)
pascal26:6: error: class method not found: __name__
  near:  base name:  PyException  __name__ >>>

Exception.__name__ on the CLASS (rather than on type(e)) is not supported. Also worth noting the diagnostic itself prints the mangled name, which is how the leak was first spotted — the error text is echoing rewritten source back at the user.

Shape of the fix

The lexer rewrite is the right mechanism (it is what makes Exception a language builtin rather than a colliding library class), but a rename needs a display name mapped back: whatever surfaces __name__ / repr() should report Exception for the class the rewrite produced. One mapping, at the point where the class name becomes user-visible, rather than unwinding the rename.

Grep for the other surfaces before closing: __name__, repr(), str() of a class, traceback rendering, and anything that formats an uncaught exception — the fix is only complete if all of them agree, and this was found through two of them by accident.

Gate

The program above matches CPython line for line, raise ValueError still reports ValueError, and an UNCAUGHT base Exception prints CPython's name in whatever traceback pxx emits.

Resolution (2026-08-14)

Re-measured first, as the handoff asked. Variant C had already fixed the two symptoms in the title: type(e).__name__ and repr(e) both report Exception and Exception('plain'), matching CPython. What was left was the last line of the summary — Exception.__name__ on the CLASS — and it was two sites, not one:

spelling before
Exception.__name__ (a static class reference) error: class method not found: __name__
cls = MyErr; cls.__name__ (a class in a VARIABLE) AttributeError: type object 'MyErr' has no attribute '__name__'

The second was found by sweeping shapes rather than by the report, and fixing only the first would have left it — the class-in-a-variable route is a VT_CLASSREF variant resolved at run time, not a compile-time classref.

Still refused, deliberately out of scope: int.__name__ / str.__name__ — a builtin type name parses as a conversion call, which is the separate known site [[project_nilpy_five_builtin_type_names_are_also_pylib_procs]] describes.

Gate: test/test_nilpy_class_dunder_name.npy (+ .expected from CPython), covering both routes and asserting they agree with the instance spellings; wired into test-nilpy. Pinned as v302 so the pylib half reaches Track B.

Log