Skip to content

Consistently treat let properties as immutable within the type checker #73609

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

Conversation

DougGregor
Copy link
Member

@DougGregor DougGregor commented May 14, 2024

There are a number of places where references let properties are treated as lvalues by the type checker, and we rely on Definite Initialization to detect and diagnose inout uses of these properties. This includes:

  • let instance properties of a type within an initializer.
  • Local let properties that aren't initialized at point of declaration.

The result of modeling these as lvalues in the type checker is that we could end up selecting a mutating function for a let base, which would then get diagnosed in Definite Initialization. If there was an alternative non-mutating overload that is otherwise not preferred, it will essentially get ignored.

Rework our handling of let properties so that they are treated as rvalues uniformly except when they are being assigned to. This moves the inout/mutating checking up into the type checker, allowing overload resolution to choose appropriately.

Fixes rdar://127258363, a source compatibility issue with AsyncIteratorProtocol.next() that uncovered this issue.

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test source compatibility

/// Expressions that are in the left-hand side of an assignment
/// expression. This is used to help distinguish assignments from
/// any other potential use that might need lvalues.
llvm::SmallPtrSet<Expr *, 2> TargetOfAssignExprs;
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 this needs to be controlled by the solver scope because result builder generate assignments as part of the transform which means that if not controlled this can grow pretty large.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since the constraint system records parent chains maybe this could be replaced with a walk up the parent chain until it reaches assignment expression?

Copy link
Member Author

Choose a reason for hiding this comment

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

I missed the fact that we record parent chains; I'd much prefer that to having to keep solver state!

Copy link
Member Author

Choose a reason for hiding this comment

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

I did this and it's so much better, thank you!

});
},
llvm::function_ref<bool(Expr *)> isAssignTarget = [](Expr *) {
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the right default?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's the default that keeps existing code working the same way as it has before. I could remove the default and force all callers to choose.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this is going to be a non-issue if you go with walk approach since it could just be a method on a constraint system?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've cleaned this up and removed the default, thanks for the nudge.

// Find all of the entries on the left-hand side of an assignment
// expression.
if (auto assign = dyn_cast<AssignExpr>(expr)) {
recordAssignmentTargets(assign->getDest());
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks similar to what happens with argument labels, should probably be moved into ConstraintGenerator::visitAssignExpr itself.

Copy link
Member Author

Choose a reason for hiding this comment

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

This code is gone now, since I'm using parent pointers

@@ -4307,6 +4313,49 @@ namespace {
return Action::Continue(expr);
}

/// Walk the destination of an assignment expression, recording
/// each of the
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: incomplete comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

(Code is gone)

AbstractStorageDecl *storage, Type baseType,
DeclContext *useDC,
llvm::function_ref<bool(Expr *)> isAssignTarget = [](Expr *) {
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to getUnopenedTypeOfReference I'm not sure if that's always correct to default to true, what if argument is omitted by mistake?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've removed the defaults, thanks

if (!memberLocator)
break;

auto *anchor = getAsExpr(memberLocator->getAnchor());
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this me simplifyToAnchor instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, thanks!

@DougGregor
Copy link
Member Author

swiftlang/swift-syntax#2653

@swift-ci please test

@DougGregor
Copy link
Member Author

swiftlang/swift-syntax#2653

@swift-ci please test source compatibility

…value

This operation determines whether a particular storage declaration,
when accessed from a particular location, is mutable or not. It has a
particular semantic that `let` declarations, when accessed from an
initializer, are considered mutable even though they can only be
assigned. There is similar logic for init accessors.

Tease apart "truly mutable" from "initializable because we're in an
initializer", introducing AbstractStorageDecl::mutability() to
represent all three states. isSettable() remains available as a thin
shim over mutability() and all clients are unchanged thus far, making
this a no-op refactoring.
Stored `let` properties of a struct, class, or actor permit
'inout' modification within the constructor body after they have been
initialized. Tentatively remove this rule, only allowing such `let`
properties to be initialized (assigned to) and not treated as `inout`.

Fixes rdar://127258363.
@DougGregor DougGregor force-pushed the instance-let-not-inout-in-initializer branch from 0786510 to 39f4e38 Compare May 14, 2024 23:00
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor
Copy link
Member Author

@swift-ci please test source compatibility

@DougGregor DougGregor changed the title Instance let not inout in initializer Consistently treat let properties as immutable within the type checker May 14, 2024
…nces as lvalues

As we do with references to initializable local lets, teach the type
checker to produce ASTs where member references to initializable
instance property lets (e.g., within an initializer) treat the `let`
as an lvalue and then immediately load.

This modeling addresses a source compatibility issue uncovered in
swift-syntax, where code that *technically* has a
use-before-definition on a `let` property in an initializer wasn't
diagnosed as such because the loaded value wasn't actually used for
anything.
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor
Copy link
Member Author

@swift-ci please test source compatibility

@DougGregor
Copy link
Member Author

@swift-ci please clean smoke test macOS

@DougGregor
Copy link
Member Author

@swift-ci please clean test source compatibility

@DougGregor
Copy link
Member Author

@swift-ci please test source compatibility

@DougGregor DougGregor merged commit f2610cf into swiftlang:main May 16, 2024
3 of 5 checks passed
@DougGregor DougGregor deleted the instance-let-not-inout-in-initializer branch May 16, 2024 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants