-
Notifications
You must be signed in to change notification settings - Fork 280
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
Apply DeMorgan's Law #1552
base: main
Are you sure you want to change the base?
Apply DeMorgan's Law #1552
Conversation
4c52731
to
18935ab
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for tackling this @AppAppWorks. I have left a first set of review comments inline. Correctness-wise, I think it’s mostly missing the addition of parentheses where precedence of operators flips, eg. !(a && b || c)
should get transformed to (!a || !b) && !c
.
case _: | ||
nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case _: | |
nil | |
default: | |
nil |
This example involves recursive DeMorgan's Law applications that have not been implemented in this PR? |
Do you mean recursive because a boolean expression is nested inside a |
Do you mean that whenever the code action results in any child expression like e.g. Or do you mean that the code action will also apply recursively to all DeMorgan-able descendent expressions of the outermost DeMorgan-able expressions? e.g. |
I would expect the former. My logic would be:
With those rules, the transformation of your second example would be
|
18935ab
to
c49ecb5
Compare
The latest commit is almost a complete rewrite of the code action. I've adopted a more succinct naming convention, simplified code paths, and covered more edge cases such as adding parentheses when precedence orders are flipped after conversion, and expansion on ternary expressions. |
created `ApplyDeMorganLaw` to provide code actions for generalised DeMorgan's conversion - matches with the outermost applicable expression - supports both logical and bitwise expressions - parentheses are created minimally added SwiftOperators from swift-syntax as a new dependency to SourceKitLSP registered in `SyntaxCodeActions.allSyntaxCodeActions` registered in Sources/SourceKitLSP/CMakeLists.txt added tests in `CodeActionTests`
c49ecb5
to
39d9077
Compare
By the way I wonder whether there is any determining factor for creating a code action under |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. I think the recursive design is the correct one. I do wonder whether the implementation can be simplified a little bit by having fewer intermediate types. I left some comments inline but it could be that I missed something. Please let me know what you think.
} | ||
|
||
/// A pseudo namespace for all facilities necessary for DeMorgan conversion. | ||
private enum DeMorgan { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the enum? Making the types fileprivate
should achieve the same goal, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, that namespace was more of part of my thought process during this rewrite.
let negation = Negation(prefixExpr), | ||
let complement = Expr(expr: negation.expr).negatedPropositions(exprType: negation.exprType) | ||
{ | ||
// TODO: remove parentheses? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it’s fine to keep the parentheses. If the user wrote them – let’s keep them. But I don’t have very strong opinions here and am happy to go with whatever is easier to implement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about the backwards conversion from a complement to which this code action has added parentheses.
\.leadingTrivia, | ||
prefixExpr.leadingTrivia + prefixExpr.expression.leadingTrivia |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also include the trailing trivia of prefixExpr
? There’s Trivia.merging
that we use in other places to ensure that we don’t end up with duplicate spaces etc.
\.leadingTrivia, | |
prefixExpr.leadingTrivia + prefixExpr.expression.leadingTrivia | |
prefixExpr.leadingTrivia.merging(prefixExpr.trailingTrivia).merging(prefixExpr.expression.leadingTrivia) |
|
||
init(expr: ExprSyntax) { | ||
if let tuple = expr.as(TupleExprSyntax.self), | ||
let only = tuple.elements.only |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have singleUnlabledExpression
that extracts the expression.
let only = tuple.elements.only | |
let parenthesizedExpression = tuple.elements.singleUnlabledExpression |
/// // mapped to | ||
/// !((a && !(b is String))) | ||
/// ``` | ||
func map(negation: Negation) -> Negated { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
map
seems like a pretty ambiguous term here but I’m struggling to find anything more meaningful. My feeling is also that the negation
parameter and self
duplicate some information. Just a thought: Could Negation
just store the underlying PrefixOperatorExprSyntax
that it was created from and then offer exprType
and expr
as computed properties (or stored properties if that’s better for performance)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I stored the negated expr of PrefixOperatorExprSyntax
directly into Negation
because it's what all the intermediate data types do. It's okay to turn exprType
and expr
into computed properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
map
seems like a pretty ambiguous term here but I’m struggling to find anything more meaningful.
I had functors in mind when choosing map
as the name, regrettably I also struggled with finding a better name.
case single(TupleExprSyntax, desinglified: ExprSyntax) | ||
/// A non-single expression at its root. The expression of interest. | ||
case other(ExprSyntax) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really have to have an enum with these two cases? Wouldn’t it be easier to just operate on ExprSyntax
and have a property that looks through all parentheses? Something like
extension ExprSyntax {
// Equivalent of the `desinglified` associated value in `single`
var lookingThroughParentheses: ExprSyntax {
if let tuple = self.as(TupleExprSyntax.self),
let underlyingExpr = tuple.singleUnlabledExpression?.lookingThroughParentheses else {
return underlyingExpr
}
return self
}
// Equivalent of checking for the `single` case.
var isParenthesizedExpression: Bool {
return self.as(TupleExprSyntax.self)?.isParentheses ?? false
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During this rewrite I've found explicit nominal types tremendously helpful for my thought process because I've been confused for so many times by conflating SwiftSyntax types with domain specific concepts.
I'm open to remove these aids for thought at this stage ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While reading through the code, they confused me more than they helped me. I usually thought: Why can’t we just return the rewritten expression? Also, the map
functions taking both a syntax node and one of the types you defined confused me a bit because they seemed to reason about to very similar but only related, not equivalent types simultaneously.
You did spend more time thinking about this than I did and I might be missing something, but if you could find the energy to write an implementation without the intermediate types that you define, I would very much like to see that. Maybe it will convince me that the current design is the correct one and we find the correct pieces of documentation that I would have needed to reason about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While reading through the code, they confused me more than they helped me. I usually thought: Why can’t we just return the rewritten expression? Also, the
map
functions taking both a syntax node and one of the types you defined confused me a bit because they seemed to reason about to very similar but only related, not equivalent types simultaneously.
It's a fair observation :) I'd leaned much towards adopting the terminology of the de Morgan's law when rewriting this code action because, for me, it'd provided a better conceptual mapping to the business logic by abstracting away the minute details of SwiftSyntax. I've struggled with and bikeshedded on naming everything, the picture didn't become clearer until I did a complete divorce from SwiftSyntax parlance.
You did spend more time thinking about this than I did and I might be missing something, but if you could find the energy to write an implementation without the intermediate types that you define, I would very much like to see that. Maybe it will convince me that the current design is the correct one and we find the correct pieces of documentation that I would have needed to reason about it.
Sure, I can still expend some time doing a minor rewrite before the start of the new school term ;)
{ | ||
( | ||
self.map(negatedExpr: negatedPropositions.expr), | ||
negatedPropositions.operator.kind == .and ? .orToAnd : .andToOr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add explicit return
here and in the else
clause because swift-syntax needs to compile with Swift 5.8, which doesn’t have if
expressions. I think the same applies to a few other methods as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't SourceKitLSP compiled with Swift 5.9?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. The code just uses so many swift-syntax types that I thought I was in the swift-syntax repo.
I still think that explicit returns would help readability here, though. Especially with the returned tuple.
|
||
/// Represents the negated expression of an instance of propositions. | ||
private struct NegatedPropositions { | ||
struct Operator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the Operator
type is a little overkill here. Wouldn’t it easier to just have a function that returns the negated operator’s TokenKind
and ExprType
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need something more than TokenKind
here, (i.e. to tell AND or OR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a whole type for that though? Wouldn’t it be sufficient to have
// In `NegatedPropositions`
var exprType: ExprType? {
switch expr.operator.tokenKind {
case .binaryOperator("|"):
return .bitwise
case .binaryOperator("&"):
return .bitwise
case .binaryOperator("||"):
return .boolean
case .binaryOperator("&&"):
return .boolean
default:
return nil
}
}
And where we check for operator.kind == .and
also just check expr.operator.tokenKind == .binaryOperator("&") || expr.operator.tokenKind == .binaryOperator("&&")
.
Or alternatively, if you prefer add
extension TokenKind {
var exprType: ExprType? {
switch expr.operator.tokenKind {
case .binaryOperator("|"):
return .bitwise
case .binaryOperator("&"):
return .bitwise
case .binaryOperator("||"):
return .boolean
case .binaryOperator("&&"):
return .boolean
default:
return nil
}
}
var isAnd: Bool {
switch expr.operator.tokenKind {
case .binaryOperator("&"):
return .bitwise
case .binaryOperator("&&"):
return .boolean
default:
return nil
}
}
}
The set of and
/or
operators hasn’t changed since C was introduced in the 70s (or even before in other language), so I don’t think we need an abstraction layer for it.
leftNegated = Expr(expr: infixExpr.leftOperand).negated(exprType: self.operator.exprType) | ||
rightNegated = Expr(expr: infixExpr.rightOperand).negated(exprType: self.operator.exprType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the same in both if
branches, so we should be able to extract it to the definition of leftNegated
.
} | ||
|
||
/// Represents the negated expression of an instance of propositions. | ||
private struct NegatedPropositions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the NegatedPropositions
type at all? It seems like all we ever do with it is to pass it into Expr.map
, so instead of having the NegatedPropositions
type, could we just have a function that returns the negated Expr
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to tell from negated propositions the type of its operator to determine whether we need to add extra parentheses to preserve precedence order. We can turn operator
into a computed property though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn’t you do that by running the negation and then when realizing that you might need to add operators, get the transformed operator using something like expr.as(BinaryExprSyntax.self)?.operator
? That way we wouldn’t have to pass around redundant state.
In this rewrite it was a deliberate decision to decouple domain specific concepts from Swift Syntax's parlance by creating explicit nominal types, as I thought it'd lay a good foundation for future implementations of other boolean/bitwise algebraic transformations, such as association, distribution, absorption, etc. |
fixed #1243