Skip to content

Python writes a base list naming the class being declared, which raises NameError on import #64

Description

@matt-edmondson

What happens

PythonGenerator writes a class's interfaces into its base list verbatim. When an interface names the class being declared — the self-type idiom — the generated module raises on import, because Python evaluates a base list eagerly.

Input:

ClassDeclaration length = new("Length") { /* … */ };
length.Interfaces.Add(TypeReference.Parse("IVector0<Length<T>, T>"));

Output:

class Length(IVector0[Length[T], T]):
    Value: T = None

Running it:

NameError: name 'Length' is not defined

Not a warning and not a runtime edge case — the module cannot be imported at all.

Why this shape and not an exotic one

IVector0<TSelf, T> is how an interface gives a method the implementing type as its return type — TSelf Create(T value) rather than IVector0 Create(T value). Every one of the 212 generated quantities in ktsu.Semantics is declared this way, and it is ordinary in C#, C++ (CRTP) and Rust (trait Foo where Self: Sized). It is not a corner.

The same is true of any mutually-referential pair, which needs no generics at all:

class Node(Visitor[Node]):   # NameError

Why it is Python's to solve rather than the AST's

The AST is right: the class does implement an interface named over itself, and six targets write that correctly. Python is the one that evaluates the base list before the name exists.

Python has two answers and the generator currently uses neither:

  1. from __future__ import annotations — makes annotations lazy, which fixes a self-reference in a field or return type but not in a base list, since that is an expression rather than an annotation.
  2. A string — class Length(IVector0["Length[T]", "T"]) — which typing resolves lazily. This is the one that works here, and is what typing.Generic consumers write for exactly this case.

So the fix is narrow: when a base names the class being declared, write that argument as a string. Everything else about the base list stays as it is.

Missing coverage

There is no Python test with a self-referential interface, and no Python equivalent of CGeneratedSourceCompilesTests — nothing runs what the generator writes. python3 is as available as g++ and go, and importing a generated module is the same shape of check the other four targets already get: it would have caught this, and it is the only way to catch the class of error where the text looks right and the interpreter disagrees.

How it was found

Building one ktsu.Semantics quantity as a neutral AST and writing it in all seven targets. Five produce what they should; this is one of the two that do not. The other is #63 (Go).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions