Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] More precise inference for classes with non-class metaclasses #15138

Merged
merged 4 commits into from
Jan 9, 2025

Conversation

InSyncWithFoo
Copy link
Contributor

Summary

Resolves #14208.

Test Plan

Markdown tests.

Copy link
Contributor

github-actions bot commented Dec 25, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@AlexWaygood AlexWaygood added the red-knot Multi-file analysis & type inference label Dec 25, 2024
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thank you! Will apply my diagnostic wording nit and then merge.

@carljm carljm enabled auto-merge (squash) January 8, 2025 17:22
@carljm
Copy link
Contributor

carljm commented Jan 8, 2025

Oops, looks like this needs a rebase before it can land. I don't have time to do that right now, but will merge if you're able to do it.

@carljm
Copy link
Contributor

carljm commented Jan 8, 2025

Sorry, I should have clarified that it's not just a rebase -- the rebase causes logical conflicts that make compilation fail.

auto-merge was automatically disabled January 8, 2025 20:31

Head branch was pushed to by a user without write access

@InSyncWithFoo
Copy link
Contributor Author

InSyncWithFoo commented Jan 8, 2025

[...] the rebase causes logical conflicts that make compilation fail.

Indeed, now there's a panic that I can't make heads or tails of:

metaclass.md - Non-class

  crates/red_knot_python_semantic/resources/mdtest/metaclass.md:169 panicked at /home/runner/.cargo/git/checkouts/salsa-61760caba2b17ca5/88a1d77/src/runtime.rs:335:13

Here's the code in question:

def f(*args, **kwargs) -> int: ...  # 169 (not even modified)

class A(metaclass=f): ...           # 171 (also not modified)

I'm also getting "Module `knot_extensions` has no member `static_assert`" errors, but only locally (Windows). Does the new Python API require some kind of configuration?

@carljm
Copy link
Contributor

carljm commented Jan 8, 2025

now there's a panic

I can repro this, will take a couple minutes to look at it and see if I can understand it.

I'm also getting "Module `knot_extensions` has no member `static_assert`" errors, but only locally (Windows). Does the new Python API require some kind of configuration?

No, but I suspect this is related to the use of a symlink to include knot_extensions.pyi in typeshed; we should probably just copy instead. Cc @sharkdp

@carljm
Copy link
Contributor

carljm commented Jan 8, 2025

The panic is due to a Salsa query cycle. Still looking into why it's new after the rebase.

@sharkdp
Copy link
Contributor

sharkdp commented Jan 8, 2025

No, but I suspect this is related to the use of a symlink to include knot_extensions.pyi in typeshed; we should probably just copy instead. Cc @sharkdp

Strange that our CI tests on Windows seem to have no problem with it? I can take a look tomorrow.

@carljm
Copy link
Contributor

carljm commented Jan 9, 2025

Ok, I tracked down the cycle. It's new with the rebase because we are now doing proper call binding. What happens is that we call the metaclass f, which takes *args, and the behavior of call binding is to create a bound type for the variadic parameter *args which is the union of the types of all arguments which map to that variadic parameter. In this case that is all three of self_ty, bases, and namespace. In constructing that union, we end up checking whether namespace (which is dict) and self_ty (which is the class A) are subtypes of each other, and this check ends up needing to know the metaclass of A, which obviously leads to a cycle.

I think this reveals a bug in the PR that I hadn't noticed. The first argument to the metaclass is not the actual class object, as modeled in the PR via self_ty. That would be pretty circular at runtime, too, since the metaclass is responsible for creating the class object! The first argument to the metaclass is a string, the name of the new class:

>>> def f(*a):
...     print(a)
...
>>> class A(metaclass=f): ...
...
('A', (), {'__module__': '__main__', '__qualname__': 'A', '__firstlineno__': 1, '__static_attributes__': ()})

If we correctly reflect this by passing Type::string_literal(db, self.name(db)) as the first argument instead of Type::class_literal(self), the cycle goes away and the tests pass.

@carljm
Copy link
Contributor

carljm commented Jan 9, 2025

For future reference, the technique I found most effective for tracking down the cause of the cycle was simply adding dbg! statements in the red-knot code to bisect where the panic occurred (I found it occurred in the metaclass.call(...) line in the non-class metaclass branch of Class::try_metaclass, then I added dbg! statements inside Type::call and discovered it was happening inside bind_call, then I added dbg! statements inside bind_call until I figured out that it was happening when adding the third argument to the union of argument types for *args, etc.)

Using query tracing (in red-knot or Salsa) was ineffective in this case, because the cycle was a single-query cycle: it was just Class::try_metaclass calling itself; there were some intervening function call layers, but no intervening Salsa queries.

@carljm carljm enabled auto-merge (squash) January 9, 2025 00:30
@carljm carljm merged commit 21aa12a into astral-sh:main Jan 9, 2025
20 checks passed
@InSyncWithFoo InSyncWithFoo deleted the rk-metaclasses branch January 9, 2025 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
4 participants