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

Add keypath method and initializer syntax under an experimental feature flag keypathWithMethodMembers #2950

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

Conversation

amritpan
Copy link
Member

@amritpan amritpan commented Jan 22, 2025

This accompanies the compiler implementation for Method and Initializer Keypaths which extends keypath usage to include references to methods and initializers:

struct S {
  static let millenium = 3
  var year = 2024
  init() {}
  init(val value: Int = 2024) { year = value }
  
  func add(this: Int) -> Int { this + this}
  func add(that: Int) -> Int { that + that }
  static func subtract(_ val: Int) -> Int { return millenium - val }
  nonisolated func nonisolatedNextYear() -> Int { return year + 1 }
  consuming func consume() { print(year) }

  subscript(index: Int) -> Int { return year + index }
}

let kp1: KeyPath<S, () -> S> = \S.Type.init
let kp2: KeyPath<S, S> = \S.Type.init()
let kp3: KeyPath<S, (Int) -> S> = \S.Type.init(val:)
let kp4: KeyPath<S, S> = \S.Type.init(val: 2025)
let kp5: KeyPath<S, (Int) -> Int> = \S.add(this:)
let kp6: KeyPath<S, Int> = \S.add(that: 1)
let kp7: KeyPath<S, Int> = \S.Type.subtract(1)
let kp8: KeyPath<S, Int> = \S.nonisolatedNextYear()
let kp9: KeyPath<S, Int> = \S.nonisolatedNextYear().signum()
let kp10: KeyPath<S, String> = \S.nonisolatedNextYear().description
let kp11: KeyPath<S, ()> = \S.consume()
let kp12: KeyPath<S, Int> = \S.Type.init()[1]

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

The implementation looks good to me. I have a few small comments inline.

Child(
name: "leftParen",
kind: .token(choices: [.token(.leftParen)]),
isOptional: true
),
Copy link
Member

Choose a reason for hiding this comment

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

Why is the left parenthesis optional? Wouldn’t we just have a normal member component if we don’t have parentheses?

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 modeled this after .funcCallExpr but you're right and I will make this non optional. @ahoppen I also have 2 related questions:

.keyPathPropertyComponent has a .genericArgumentClause but I don't see any examples of it being used in the tests. Do you happen to know when that is used? It seems like a generic on the root (eg: \Box<Int>.item) is already being handled by KeyPathExpr.

Is there a way to make .labeledExprList optional? I forgot to account for this - partially applied methods parse their method name and arg list into DeclReferenceExpr and do not have a LabeledExprList. I'm inclined to create a second node to handle this (eg: .keyPathPartiallyAppliedComponent).

Copy link
Member

Choose a reason for hiding this comment

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

.keyPathPropertyComponent has a .genericArgumentClause but I don't see any examples of it being used in the tests. Do you happen to know when that is used? It seems like a generic on the root (eg: \Box<Int>.item) is already being handled by KeyPathExpr.

I don’t think a generic clause is actually valid in key path properties and maybe we should remove it. @rintaro Can you think of a reason why KeyPathPropertyComponentSyntax should have a generic arguments clause?

Is there a way to make .labeledExprList optional? I forgot to account for this - partially applied methods parse their method name and arg list into DeclReferenceExpr and do not have a LabeledExprList. I'm inclined to create a second node to handle this (eg: .keyPathPartiallyAppliedComponent).

I’m not sure I understand. If there are no parentheses, we should be able to parse the component as a KeyPathPropertyComponentSyntax. If there are parentheses and no arguments, arguments can just be an empty list. And if there are arguments … then it doesn’t need to be optional.

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’m not sure I understand. If there are no parentheses, we should be able to parse the component as a KeyPathPropertyComponentSyntax. If there are parentheses and no arguments, arguments can just be an empty list. And if there are arguments … then it doesn’t need to be optional.

Should partial arguments should also be KeyPathPropertyComponentSyntax? method(_:) and method(arg:) for unapplied keypath methods are being parsed as DeclReferenceExpr with DeclNameArgumentListSyntax arguments instead of LabeledExprList. I have pushed changes that reflect this, unsure if there are any ramifications of doing it this way, but the Swift and c++ parser outputs match.

CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift Outdated Show resolved Hide resolved
Sources/SwiftParser/Expressions.swift Outdated Show resolved Hide resolved
Sources/SwiftParser/Expressions.swift Outdated Show resolved Hide resolved
Tests/SwiftParserTest/ExpressionTests.swift Show resolved Hide resolved
@amritpan
Copy link
Member Author

amritpan commented Feb 4, 2025

Added additional tests to ensure Swift and c++ parsers match.

CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift Outdated Show resolved Hide resolved
Sources/SwiftParser/Expressions.swift Outdated Show resolved Hide resolved
Sources/SwiftParser/Expressions.swift Outdated Show resolved Hide resolved
Sources/SwiftParser/Expressions.swift Outdated Show resolved Hide resolved
Sources/SwiftParser/Expressions.swift Outdated Show resolved Hide resolved
Comment on lines 2064 to 2087
while true {
let token = lookahead.peek().rawTokenKind
if token == .endOfFile || lookahead.atStartOfLine {
break
}
if token == .leftParen {
hasLParen = true
}
if token == .colon {
lookahead.consumeAnyToken()
// If there's a colon followed by a right parenthesis, it is
// a partial application and should be parsed as a property.
if lookahead.peek().rawTokenKind == .rightParen {
return false
}
}
if token == .rightParen {
hasRParen = true
}
lookahead.consumeAnyToken()
}
// If parentheses exist with no partial application pattern,
// parse as a key path method.
return hasLParen && hasRParen ? true : false
Copy link
Member

Choose a reason for hiding this comment

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

This seems problematic to me. I haven’t fully though it through but it has the potential for an unbounded lookahead, which could be very bad for parser performance and the use of consumeAnyToken to eat any uncovered token also seems like a pitfall to me.

What I would suggest is that in your actual parse* method, you call parseDottedExpressionSuffix outside of the if. That would parse any compound names, ie. unapplied function references. If we are at a parenthesis after that, continue parsing arguments for an applied key path reference. That way, you can also parse key paths like \Foo.t(a:)(2), mirroring the following valid Swift code

struct Foo {
  func t(a: Int) {
    self.t(a:)(2)
  }
}

Or am I missing something with that approach?

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 makes sense and I've updated the code to reflect this. For reasons I can't reproduce now, parseDottedExpressionSuffix did not work for me initially and so I wrote this new method. Regardless it works now. All good things to know about Lookahead as well. Thank you!

@amritpan amritpan force-pushed the method-keypaths branch 4 times, most recently from 6cf481b to 6790783 Compare February 5, 2025 23:06
@amritpan amritpan requested a review from ahoppen February 5, 2025 23:06
Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

That looks great and is much simpler as well. Just two minor comments, everything else is great.

Sources/SwiftParser/Expressions.swift Outdated Show resolved Hide resolved
Tests/SwiftParserTest/ExpressionTests.swift Outdated Show resolved Hide resolved
Release Notes/602.md Outdated Show resolved Hide resolved
Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

Looks good to me 🚀

@ahoppen
Copy link
Member

ahoppen commented Feb 6, 2025

@swift-ci Please test

@amritpan
Copy link
Member Author

amritpan commented Feb 6, 2025

Not sure why this test is failing - is it due to my imports in Expressions.swift?

@ahoppen
Copy link
Member

ahoppen commented Feb 6, 2025

Oh, I just didn’t test it with your PR in the compiler repo. Let’s try again.

swiftlang/swift#78823

@swift-ci Please test

@ahoppen
Copy link
Member

ahoppen commented Feb 6, 2025

Looks like you need to rebase this PR + re-generate the sources to adjust it for #2926.

@amritpan
Copy link
Member Author

amritpan commented Feb 7, 2025

Rebased and regenerated, but running into this failure locally:

./swift-syntax-dev-utils local-pr-precheck
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product 'swift-syntax-dev-utils' complete! (0.14s)
** Running code generation **
** Building target SwiftSyntax-all **
Building for debugging...
[55/55] Emitting module SwiftSyntaxBuilder
Build of target: 'SwiftSyntax-all' complete! (9.33s)
** Building target Examples-all **
Building for debugging...
error: couldn't build /.../swift-project/swift-syntax/Examples/.build/arm64-apple-macosx/debug/SwiftSyntax.build/sources because of missing inputs: /.../swift-project/swift-syntax/Sources/SwiftSyntax/Raw/RawSyntaxArena.swift, /.../swift-project/swift-syntax/Sources/SwiftSyntax/ArenaAllocatedBuffer.swift

@amritpan amritpan requested a review from ahoppen February 7, 2025 00:00
@ahoppen
Copy link
Member

ahoppen commented Feb 7, 2025

Could you try if deleting Examples/.build fixes the issue you’re seeing locally?

@ahoppen
Copy link
Member

ahoppen commented Feb 7, 2025

swiftlang/swift#78823

@swift-ci Please test

1 similar comment
@ahoppen
Copy link
Member

ahoppen commented Feb 7, 2025

swiftlang/swift#78823

@swift-ci Please test

@amritpan
Copy link
Member Author

amritpan commented Feb 7, 2025

Yes, deleting Examples/.build fixed the issue locally.

@ahoppen
Copy link
Member

ahoppen commented Feb 8, 2025

Sorry, looks like you picked a bad time for your PR right now as you’re racing with @rintaro’s refactorings. Can you rebase + re-generate once more to pick up #2926?

@amritpan
Copy link
Member Author

amritpan commented Feb 8, 2025

Yes, rebased and regenerated!

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