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 3 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

private CallExpr call;

InvokedClosureExpr() {
call.getFunction() = this and
Copy link
Preview

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

[nitpick] The condition call.getFunction() = this could be inefficient as it requires checking all CallExpr nodes. Consider using a more targeted approach or adding an index if this becomes a performance bottleneck.

Suggested change
call.getFunction() = this and
/**
* Associates a CallExpr with its function.
*/
private predicate isFunctionOfCall(Expr function, CallExpr call) {
call.getFunction() = function
}
InvokedClosureExpr() {
isFunctionOfCall(this, call) and

Copilot uses AI. Check for mistakes.

// _If_ the invoked expression has the type of a closure, then we propagate
// the surrounding types into the closure.
exists(int arity, TypePath path0 |
ce.getTypeAt(TypePath::nil()).(DynTraitType).getTrait() instanceof FnOnceTrait
Copy link
Preview

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

This condition performs multiple method calls and type checks in sequence. Consider caching the result of ce.getTypeAt(TypePath::nil()) or restructuring to avoid repeated computation.

Suggested change
ce.getTypeAt(TypePath::nil()).(DynTraitType).getTrait() instanceof FnOnceTrait
let cachedType = ce.getTypeAt(TypePath::nil()) |
cachedType.(DynTraitType).getTrait() instanceof FnOnceTrait

Copilot uses AI. Check for mistakes.

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.

1 participant