Skip to content

[red-knot] Further optimize *-import visibility constraints #17375

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

Merged
merged 3 commits into from
Apr 15, 2025

Conversation

AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Apr 13, 2025

Summary

Fixes #17322. Remove any need for snapshotting when applying visibility constraints to * imports. This also ends up simplifying the code!

Test Plan

Existing tests all pass. I'd appreciate a careful review, though. Is it definitely safe to remove these calls, as I am doing currently in the PR?

E.g. the UseDefMapBuilder::negate_star_import_visibility_constraint method on main has this call:

        self.scope_start_visibility = self
            .visibility_constraints
            .add_and_constraint(self.scope_start_visibility, negated_constraint);

and UseDefMapBuilder::merge has this call:

        self.scope_start_visibility = self
            .visibility_constraints
            .add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);

Both calls are currently removed in this PR for the "fast path" for newly added *-import definitions, since UseDefMapBuilder::negate_star_import_visibility_constraint is removed as part of this PR, and UseDefMapBuilder::merge is no longer called in the fast path. If it's not safe to remove these calls from the fast path, is there a test I could add that passes on main, but fails with them removed?

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Apr 13, 2025
Copy link
Contributor

github-actions bot commented Apr 13, 2025

mypy_primer results

No ecosystem changes detected ✅

@AlexWaygood
Copy link
Member Author

AlexWaygood commented Apr 13, 2025

3% speedup on the cold benchmark 🥳 https://codspeed.io/astral-sh/ruff/branches/alex%2Foptimize-star-imports

@AlexWaygood AlexWaygood added the performance Potential performance improvement label Apr 13, 2025
@AlexWaygood AlexWaygood marked this pull request as ready for review April 13, 2025 18:04
Copy link
Contributor

@sharkdp sharkdp left a comment

Choose a reason for hiding this comment

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

Thank you, this looks correct! Great that we can reclaim even more performance.

self.scope_start_visibility tracks the visibility of x_new = <unbound> bindings of symbols that are not yet defined (symbols that are already defined, track visibility of their own x_defined = <unbound> binding). This is important when we model something like

if True:
    x = 1
else:
    # At this point in control flow, we have a scope_start_visibility
    # of `~[True]`, which we'll use for the `x = <unbound>` binding
    # that will be implicitly created for this control flow branch
    # when we merge control flow with the `if` branch below.

# a use of `x` here sees `x = 1` with visibility `[True]` and
# `x = <unbound>` with visibility `~[True]`.

The important thing is that scope_start_visibility returns to AlwaysTrue after this if-else branch. x can not be unbound, but new symbols would still be unbound. You can basically ask: if I would see a use of has_not_been_bound_yet at this point in control flow, would I still be able to see the implicit has_not_been_bound_yet = <unbound> binding at the scope start? And the answer for this example is yes. Things could be different if there would be terminal statements inside the branches:

def _():
    if True:
        return

    # scope_start_visibility would be AlwaysFalse here

For the star-import construct that we're modeling, the scope-start-visibility should not be affected (after control flow has merged), since there are no terminal statements inside. A use of has_not_been_bound_yet after this construct would have the same visibility of the scope start than before:

if <condition>:
    from module import X
else:
    # empty

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.

Very nice, thank you!!

@@ -714,32 +714,32 @@ impl<'db> UseDefMapBuilder<'db> {
.add_and_constraint(self.scope_start_visibility, constraint);
}

#[must_use = "A `*`-import visibility constraint must always be negated after it is added"]
pub(super) fn record_star_import_visibility_constraint(
pub(super) fn record_and_negate_star_import_visibility_constraint(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be useful to future-us to add a doc comment to this method explaining what it is doing and why it is safe to do it, in the limited cases where we use this function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added some docs! Sadly this PR is no longer net-negative in terms of whether it adds or removes lines of code, but so it goes 🥲

Would appreciate a quick review from you or @sharkdp to double-check everything in the doc-comment is accurate!

@AlexWaygood
Copy link
Member Author

I think there are probably some optimisations we could also make for the "slow path" where the * import redefines a symbol that already exists in the global namespace. Even in the slow path, we're probably doing much more work than we need to by snapshotting all definition/declaration states -- we really only need to snapshot the definition/declaration states for a single symbol.

But I don't know if it's worth the complexity cost to make that optimisation, since I think it would require adding more APIs to the use-def map that would only be used in this one place, and we probably don't hit the slow path very often at all.

Copy link
Contributor

@sharkdp sharkdp left a comment

Choose a reason for hiding this comment

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

The new comment looks great, thank you

@AlexWaygood AlexWaygood merged commit 79b9211 into main Apr 15, 2025
23 checks passed
@AlexWaygood AlexWaygood deleted the alex/optimize-star-imports branch April 15, 2025 11:32
dcreager added a commit that referenced this pull request Apr 15, 2025
* main: (31 commits)
  [red-knot] Add some knowledge of `__all__` to `*`-import machinery (#17373)
  Update taiki-e/install-action digest to be7c31b (#17379)
  Update Rust crate mimalloc to v0.1.46 (#17382)
  Update PyO3/maturin-action action to v1.49.1 (#17384)
  Update Rust crate anyhow to v1.0.98 (#17380)
  dependencies: switch from `chrono` to `jiff`
  Update Rust crate bstr to v1.12.0 (#17385)
  [red-knot] Further optimize `*`-import visibility constraints (#17375)
  [red-knot] Minor 'member_lookup_with_policy' fix (#17407)
  [red-knot] Initial support for `dataclass`es (#17353)
  Sync vendored typeshed stubs (#17402)
  [red-knot] improve function/bound method type display (#17294)
  [red-knot] Move relation methods from `CallableType` to `Signature` (#17365)
  [syntax-errors] `await` outside async functions (#17363)
  [red-knot] optimize is_subtype_of for literals (#17394)
  [red-knot] add a large-union-of-string-literals benchmark (#17393)
  Update pre-commit dependencies (#17383)
  [red-knot] mypy_primer: Fail job on panic or internal errors (#17389)
  [red-knot] Document limitations of diagnostics-silencing in unreachable code (#17387)
  [red-knot] detect unreachable attribute assignments (#16852)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Potential performance improvement ty Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[red-knot] Consider further optimising visibility constraints applied to *-import definitions
3 participants