Skip to content

Rust: Implement type inference for closures and calls to closures #20130

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

paldepind
Copy link
Contributor

@paldepind paldepind commented Jul 27, 2025

This PR implements type inference for closure expressions, calls to closures, and correctly handles FnOnce trait bounds.

A few notes on the implementation and the PR:

  • In Rust parameters for functions in function types are represented as tuples. For instance, the FnOnce trait has a type parameter Args which is a tuple of all the arguments. That is reflected in our implementation as well, i.e., we re-use our tuple types for parameters.
  • Rust represents first-class functions using the FnOnce, FnMut, andFn traits. They form a trait hierarchy with FnOnce at the top. FnOnce uses an associated type for the return type and since we don't support associated types inherited in subtraits (tests for this was added in Rust: Fix type inference for trait objects for traits with associated types #20122) we can right now only really understand the FnOnce trait and not the others.
  • Closures are given a dyn FnOnce type. That is, instead of creating some new type for closures we just reuse dyn. The Rust language itself says that closures have some internal type that can't be written by users, and all that known about it is that it implements some of the FnOnce/Fn/FnMut traits. dyn gives us this effect. rust-analyzer shows closures as having in impl FnOnce type. I think that makes a bit more sense than dyn, but using dyn was simpler.
  • It would be better to use the Fn trait for closures, as it is a subtrait of the others, but, again, we can't understand Fn yet.

DCA seems fine, though we only get a 0.04% point increase in resolved calls, which is less than I had hoped.

@github-actions github-actions bot added the Rust Pull requests that update Rust code label Jul 27, 2025
@paldepind paldepind marked this pull request as ready for review July 28, 2025 06:00
@Copilot Copilot AI review requested due to automatic review settings July 28, 2025 06:00
@paldepind paldepind requested a review from a team as a code owner July 28, 2025 06:00
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements type inference for closure expressions and calls to closures in Rust, including proper handling of FnOnce trait bounds. The implementation reuses tuple types for function parameters and represents closures as dyn FnOnce types.

  • Adds type inference support for closure expressions, their parameters, and return types
  • Implements type propagation for calls to closures and higher-order functions
  • Introduces handling of the FnOnce trait with syntactic sugar for function types

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
closure.rs New test file with comprehensive closure type inference test cases
main.rs Removes old closure test module and adds reference to new test file
TypeMention.qll Adds support for parenthesized argument lists and FnOnce trait handling
TypeInference.qll Implements core closure type inference logic and call expression handling
Stdlib.qll Defines the FnOnce trait class for type system integration
type-inference.expected Expected test output showing inferred types for closure expressions

Copy link
Contributor

@geoffw0 geoffw0 left a comment

Choose a reason for hiding this comment

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

Some questions to make sure I'm understanding things.

The relatively small improvement in DCA suggests there may still be gaps to get coverage of the most common cases???

mod simple_closures {
pub fn test() {
// A simple closure without type annotations or invocations.
let my_closure = |a, b| a && b;
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this test case show us anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It shows (in the expected file) what types will be inferred for a simple closure that isn't called.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, shame it doesn't show up in an annotation, but 👍.

@@ -2459,46 +2459,7 @@ pub mod pattern_matching_experimental {
}
}

mod closures {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why have these test cases been deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the stuff being tested by these examples are covered by the new tests for closures. There was quite a lot of "extra" setup in these tests, for what was actually being tested, so I think the new tests are preferable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes they were somewhat close to some real world examples I'd encountered. I guess I'd like reassuring that they would indeed work right now, whether or not they're part of our test suite.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I should have been more clear. They unfortunately still don't work, due to the "Another reason" that I mention in #20130 (comment).

The case is covered by the

        let f = |x| x + 1; // $ MISSING: type=x:i64 target=add
        let _r2 = apply_two(f); // $ target=apply_two type=_r2:i64

test, which is essentially a "minimal" variant of the existing test. It test passing an unannotated closure to a function with a known type.

)
or
// _If_ the invoked expression has the type of a closure, then we propagate
// the surrounding types into the closure.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to understand how these differs from the cases above a bit better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The key difference is the direction.

  • The cases above propagate type information out from the invoked expression (which implements FnOnce) to the arguments and the entire call.
  • The cases below propagate type information in to the invoked expression. This however we only do if the invoked expression has a closure type (isn't some other type that implements FnOnce).

Propagating type information into the invoked expression for arbitrary FnOnce implementations would make sense, but we don't have that machinery as of right now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great, thanks for the explanation.

Co-authored-by: Geoffrey White <40627776+geoffw0@users.noreply.github.com>
@paldepind
Copy link
Contributor Author

The relatively small improvement in DCA suggests there may still be gaps to get coverage of the most common cases???

My guess is that a big reason is that we only support FnOnce and not Fn and FnMut

Another reason could be that we can't infer the type of a closure from it being passed to an annotated function. This is shown on line 62 in the test and is an instance of a more general problem: When an argument is passed to a function where the function expects the argument to implement some trait, we don't propagate information from the trait bound back to the argument. This affects closures a lot since they are always given as trait bounds on parameter types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Rust Pull requests that update Rust code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants