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

[SR-8089] Fix-it misplaces .self if name of type is wrapped in String interpolation parentheses #50622

Open
swift-ci opened this issue Jun 23, 2018 · 9 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself diagnostics QoI Bug: Diagnostics Quality of Implementation

Comments

@swift-ci
Copy link
Collaborator

Previous ID SR-8089
Radar rdar://problem/29977680
Original Reporter Matt Rips (JIRA User)
Type Bug
Status In Progress
Resolution
Environment

Xcode 10.0 beta

Swift 4.2

Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug, DiagnosticsQoI
Assignee Matt Rips (JIRA)
Priority Medium

md5: 655de83d0fb8a7d6e8aaaa9d0d58016a

Issue Description:

NOTE: This issue looks like an awkward corner case of the fix-it system. Since it isn't doing real harm, it probably is not worth the time to resolve it properly. There probably is a hack that would give a quick resolution, but that approach might have unintended side effects.

BACKGROUND: When correctly accessing a type as a value for purposes of String interpolation (or otherwise), .self is appended to the type name. Example:

print("Corrupt data reported by \(MyClass.self).") // correct syntax

If .self is omitted, the compiler reports an error and offers a fix-it.

print("Corrupt data reported by \(MyClass).")
// ERROR: Expected member name or constructor call after type name 
// FIXIT: Use '.self' to reference the type object

ISSUE: Mashing the fix-button results in .self being appended. However, the issue is that, if the type name is wrapped within the String interpolation parentheses, then the .self is appended after the parentheses rather than to the type name.

print("Corrupt data reported by \(MyClass).self.") // incorrect placement

Of course, the correct placement of the .self is immediately after the type name, as shown in the initial code snippet, above. With the .self placed outside the parentheses, a new error condition results.

Partial Diagnosis:

I don't know enough about the compiler code base and C++ to resolve the issue, but it looks to me like the issue centers on the code at:

lib/Sema/MiscDiagnostics.cpp at line 825:

// Add fix-it to insert ".self".
auto diag = TC.diagnose(E->getEndLoc(), diag::add_self_to_type);
 if (E->canAppendPostfixExpression()) {
       diag.fixItInsertAfter(E->getEndLoc(), ".self");
 } else {
       diag.fixItInsert(E->getStartLoc(), "(");
       diag.fixItInsertAfter(E->getEndLoc(), ").self")
 }

It appears that the call to canAppendPostfixExpression() is returning true. Inside the canAppendPostfixExpression() function (in file lib/AST/Expr.cpp at line 683), the return value switches on the result of a call to getKind(), and in the case of ExprKind::InterpolatedStringLiteral, the return value is true. But, I'm not even sure that that is the case to which this sort of expression resolves. It seems to me that, given the error reported, the expression must be resolving to something like a MetaType, but I don't yet know enough about the compiler's approach to resolving expressions. So, my investigation ends there.

If this issue (and its potential resolution) is of a simple enough nature to qualify as a starter bug, I would be game to do the learning necessary to resolve it.

@belkadan
Copy link
Contributor

brentdax (JIRA User), CodaFi (JIRA User): this seems related to the work on avoiding tuple expressions in interpolations. Got time to give Matthew some pointers?

@beccadax
Copy link
Contributor

Where I would start on debugging this is: What does the AST look like right now, and which node are we looking at when we diagnose the error?

To answer the first question, put a minimal test case in a file and run "path/to/swift -frontend -dump-ast test.swift", where "test.swift" is your test case's file. What you should see (along with some other stuff representing the rest of the file) is an interpolated_string_literal_expr whose children alternate between string_literal_expr and paren_expr (with the paren_exprs having children representing the expressions inside them). Sometimes you don't end up with a ParenExpr, though, and that tends to cause weird little bugs.

To answer the second question, set a breakpoint in MiscDiagnostics and run your test file with the debugger attached to Swift; then when it hits your breakpoint, type "expr E->dump()" in the debugger. (Or stick "E->dump();" in the code and recompile. Ugly and slow, but you don't have to wrestle with the debugger.) This should print out the node we're looking at in the same format that the -dump-ast switch uses.

@swift-ci
Copy link
Collaborator Author

Comment by Matthew Rips (JIRA)

Thanks, Jordan.

Appreciate the pointers, Brent. In the AST, the paren_expr is missing. It is resolving to: type_expr type='(MyClass).Type'

I'm inspired, and will chase it down.

@beccadax
Copy link
Contributor

Matt Rips (JIRA User), if you're still trying to chase this down, I would look at parseStringSegments() in ParseExpr.cpp. Specifically, if you skip down to the Lexer::StringSegment::Expr case and look a couple dozen lines down, you'll see a call to parseExprList(). That's where string interpolations are parsed. You might want to drop a breakpoint there and see if the parentheses are already part of the type at that point. If so, we can patch it up there; if not, we can try to figure out where it gets changed afterwards.

@swift-ci
Copy link
Collaborator Author

Comment by Matthew Rips (JIRA)

Brent Royal-Gordon, sorry to not respond sooner. I was engaged in a combination of setting up my Swift development environment (so that I could take the steps that you describe!) and learning more about the the type-checking system (like reading Doug Gregor's write up from 2012-14 and reading the code) and the Swift architecture. I'm setup, now, and know enough to perhaps be useful.

Thank you for these additional pointers. I'll report back, shortly, with findings.

@swift-ci
Copy link
Collaborator Author

swift-ci commented Jul 1, 2018

Comment by Matthew Rips (JIRA)

Brent Royal-Gordon, I've tracked down the mechanism at work, here.

The parens and the type name are folded together during type checking, as part of an ASTWalk initiated in the preCheckExpression method of TypeChecker. Eventually, the simplifyTypeExpr method of PreCheckExpression (an ASTWalker) is called. The specific portion of that method that does the folding is:

/// Simplify expressions which are type sugar productions that got parsed
/// as expressions due to the parser not knowing which identifiers are
/// type names.
TypeExpr *PreCheckExpression::simplifyTypeExpr(Expr *E) {
  ...

  // Fold (T) into a type T with parens around it.
  if (auto *PE = dyn_cast<ParenExpr>(E)) {
    auto *TyExpr = dyn_cast<TypeExpr>(PE->getSubExpr());
    if (!TyExpr) return nullptr;
    TupleTypeReprElement InnerTypeRepr[] = { TyExpr->getTypeRepr() };
    assert(!TyExpr->isImplicit() && InnerTypeRepr[0].Type &&
           "SubscriptExpr doesn't work on implicit TypeExpr's, "
           "the TypeExpr should have been built correctly in the first place");
    auto *NewTypeRepr = TupleTypeRepr::create(TC.Context,
                                              InnerTypeRepr,
                                              PE->getSourceRange());
    return new (TC.Context) TypeExpr(TypeLoc(NewTypeRepr, Type()));
  }
  ...
}

The comment leading into this method call suggests that this code is a patch to address some subtle issues earlier in the type check process; seemingly involving types not properly resolving as such.

When the minimal example arrives at this point in the type checker, the type seems to be resolving as if it were properly formed. Specifically, using `let a = (Int)`, results in the relevant part of the AST being:

(paren_expr type='<null>'

(type_expr type='<null>' typerepr='Int'))

Interestingly, the `Int` portion seems to be properly recognized as a type expression before the code, above, starts folding it together. After it leaves this section of code, the AST has it as:

(type_expr type='<null>' typerepr='(Int)')

So, what is the purpose behind this code transforming expressions that already seem to be properly formed? The comment at the beginning of the method suggests that it is trying to deal with a type declaration that has been mis-evaluated as an expression.

But, here, of course, the minimal example is syntactically incorrect. We've omitted the `.self`, and the compiler eventually complains about it. When we add the `.self`, the code, above, leaves it alone, and the parentheses are not folded into the type.

I'm going to continue to explore the mechanism at work, here. Wondering if you might have some thoughts about it.

@beccadax
Copy link
Contributor

beccadax commented Jul 1, 2018

Matt Rips (JIRA User): Good job tracing this! I actually didn't think it was very likely that the problem was in type-checking, so you found it despite my hint, not because of it.

Understanding what's happening here requires some knowledge of the compiler you probably don't have yet, so let me give you some background.

The Swift AST represents arguments to a call as a ParenExpr if there is one unlabeled argument, or a TupleExpr if there are zero arguments, one labeled argument, or more than one argument (with or without labels). This is sort of a legacy thing; in earlier versions of Swift, we had the idea that functions only ever had one argument (which might be a tuple), but that ended up not really working out as the language evolved. We've gradually removed all evidence of that behavior from the language as users see it, but the AST currently still represents arguments that way.

(This is a pet peeve of mine—at least once a week, I have to fight the impulse to open Expr.h and start writing an ArgumentListExpr.)

Now, if you look at the first lines of 'simplifyTypeExpr()', you'll notice that there's a check which is supposed to exempt argument lists from the simplification it's doing. But it's not saving us here—probably because the check doesn't realize that the segments of an InterpolatedStringLiteralExpr are destined to become argument lists and need to be treated the same way. If you can figure out how to get it to treat an InterpolatedStringLiteralExpr's segments as argument lists, I think we'll be in business.

Once you have a plan for how to fix this bug, I think these should be your next steps:

  1. Write a test which fails with the current compiler (i.e. without fixing the bug). I'd try to find a file in test/ which tests the same fix-it in other contexts, then add a test which uses a string interpolation and makes sure the fix-it is correctly positioned. We test diagnostics and fix-it positions with special comments—if you look around that file, you'll probably see enough examples to understand the syntax.

  2. Implement your bug fix and confirm that that your new test passes with the fix in place.

  3. Run all of the tests, just to make sure your fix didn't break anything else.

  1. Commit your bug fix and test to a branch, push that branch up to your fork of the Swift repo, and submit a pull request.
  1. Post a link to your pull request here and mention me again so I can kick off the next steps.

Hope this helps!

@swift-ci
Copy link
Collaborator Author

swift-ci commented Jul 3, 2018

Comment by Matthew Rips (JIRA)

Brent Royal-Gordon: I'm afraid that I'd be putting a band-aid on a small symptom of a larger bug. The code that folds (T) is doing something that is fundamentally illegal.

It is creating a single-element tuple, but there is no such thing as a single-element tuple. The documentation states that, "All tuple types contain two or more types, except for Void which is a type alias for the empty tuple type, ()."

@beccadax
Copy link
Contributor

beccadax commented Jul 3, 2018

Matt Rips (JIRA User): Internally, Swift actually does use single-element tuples purposefully in some cases (mainly for single labeled arguments). But we know they sneak through sometimes when they shouldn't. The set of refactorings needed to fix it properly is too big to do immediately, but several of us are gearing up to wage war on the root causes of these problems.

So apply your band-aid and bide your time. Honestly, if it actually fixes a single-element tuple instead of just limiting the damage, it's better than the tuple band-aid I wrote a couple weeks ago.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself diagnostics QoI Bug: Diagnostics Quality of Implementation
Projects
None yet
Development

No branches or pull requests

3 participants