diff --git a/Package.swift b/Package.swift index bd5577202bb..954ac9d9d72 100644 --- a/Package.swift +++ b/Package.swift @@ -45,6 +45,8 @@ let package = Package( .macCatalyst(.v13), ], products: [ + .library(name: "SwiftOperators", type: .static, + targets: ["SwiftOperators"]), .library(name: "SwiftParser", type: .static, targets: ["SwiftParser"]), .library(name: "SwiftSyntax", type: .static, targets: ["SwiftSyntax"]), .library(name: "SwiftSyntaxParser", type: .static, targets: ["SwiftSyntaxParser"]), @@ -118,13 +120,19 @@ let package = Package( "DeclarationAttribute.swift.gyb", ] ), + .target( + name: "SwiftOperators", + dependencies: ["SwiftSyntax", "SwiftParser", "SwiftDiagnostics"] + ), .executableTarget( name: "lit-test-helper", dependencies: ["SwiftSyntax", "SwiftSyntaxParser"] ), .executableTarget( name: "swift-parser-test", - dependencies: ["SwiftDiagnostics", "SwiftSyntax", "SwiftParser", .product(name: "ArgumentParser", package: "swift-argument-parser")] + dependencies: ["SwiftDiagnostics", "SwiftSyntax", "SwiftParser", + "SwiftOperators", + .product(name: "ArgumentParser", package: "swift-argument-parser")] ), .executableTarget( name: "generate-swift-syntax-builder", @@ -171,7 +179,13 @@ let package = Package( ), .testTarget( name: "SwiftParserTest", - dependencies: ["SwiftDiagnostics", "SwiftParser", "_SwiftSyntaxTestSupport"] + dependencies: ["SwiftDiagnostics", "SwiftOperators", "SwiftParser", + "_SwiftSyntaxTestSupport"] + ), + .testTarget( + name: "SwiftOperatorsTest", + dependencies: ["SwiftOperators", "_SwiftSyntaxTestSupport", + "SwiftParser"] ), ] ) diff --git a/Sources/SwiftOperators/Operator.swift b/Sources/SwiftOperators/Operator.swift new file mode 100644 index 00000000000..1edb78012a1 --- /dev/null +++ b/Sources/SwiftOperators/Operator.swift @@ -0,0 +1,57 @@ +//===------------------ Operator.swift ------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Names an operator. +/// +/// TODO: For now, we'll use strings, but we likely want to move this to a +/// general notion of an Identifier. +public typealias OperatorName = String + +/// Describes the kind of an operator. +public enum OperatorKind: String { + /// Infix operator such as the + in a + b. + case infix + + /// Prefix operator such as the - in -x. + case prefix + + /// Postfix operator such as the ! in x!. + case postfix +} + +/// Describes an operator. +public struct Operator { + public let kind: OperatorKind + public let name: OperatorName + public let precedenceGroup: PrecedenceGroupName? + public let syntax: OperatorDeclSyntax? + + public init( + kind: OperatorKind, name: OperatorName, + precedenceGroup: PrecedenceGroupName?, + syntax: OperatorDeclSyntax? = nil + ) { + self.kind = kind + self.name = name + self.precedenceGroup = precedenceGroup + self.syntax = syntax + } +} + +extension Operator: CustomStringConvertible { + /// The description of an operator is the source code that produces it. + public var description: String { + (syntax ?? synthesizedSyntax()).description + } +} diff --git a/Sources/SwiftOperators/OperatorError+Diagnostics.swift b/Sources/SwiftOperators/OperatorError+Diagnostics.swift new file mode 100644 index 00000000000..ce8fb3a7854 --- /dev/null +++ b/Sources/SwiftOperators/OperatorError+Diagnostics.swift @@ -0,0 +1,75 @@ +//===------------------ OperatorError.swift ---------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftDiagnostics +import SwiftParser +import SwiftSyntax + +extension OperatorError : DiagnosticMessage { + public var severity: DiagnosticSeverity { + .error + } + + public var message: String { + switch self { + case .groupAlreadyExists(let existing, _): + return "redefinition of precedence group '\(existing.name)'" + + case .missingGroup(let groupName, _): + return "unknown precedence group '\(groupName)'" + + case .operatorAlreadyExists(let existing, _): + return "redefinition of \(existing.kind) operator '\(existing.name)'" + + case .missingOperator(let operatorName, _): + return "unknown infix operator '\(operatorName)'" + + case .incomparableOperators(_, let leftGroup, _, let rightGroup): + if leftGroup == rightGroup { + return "adjacent operators are in non-associative precedence group '\(leftGroup)'" + } + + return "adjacent operators are in unordered precedence groups '\(leftGroup)' and '\(rightGroup)'" + } + } + + public var diagnosticID: MessageID { + MessageID(domain: "SwiftOperators", id: "\(self)") + } +} + +extension OperatorError { + /// Produce the syntax node at which a diagnostic should be displayed. + var diagnosticDisplayNode: Syntax { + switch self { + case .incomparableOperators(let leftOperator, _, _, _): + return Syntax(leftOperator) + + case .missingOperator(_, let node): + return node + + case .operatorAlreadyExists(_, let newOperator): + return Syntax(newOperator.syntax ?? newOperator.synthesizedSyntax()) + + case .missingGroup(_, let node): + return node + + case .groupAlreadyExists(_, let newGroup): + return Syntax(newGroup.syntax ?? newGroup.synthesizedSyntax()) + } + } + + /// Produce a diagnostic for a given operator-precedence error. + public var asDiagnostic: Diagnostic { + .init(node: diagnosticDisplayNode, message: self) + } +} diff --git a/Sources/SwiftOperators/OperatorError.swift b/Sources/SwiftOperators/OperatorError.swift new file mode 100644 index 00000000000..5dfb8746c40 --- /dev/null +++ b/Sources/SwiftOperators/OperatorError.swift @@ -0,0 +1,45 @@ +//===------------------ OperatorError.swift ---------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import SwiftSyntax + +/// Describes errors that can occur when working with user-defined operators. +public enum OperatorError: Error { + /// Error produced when a given precedence group already exists in the + /// precedence graph. + case groupAlreadyExists(existing: PrecedenceGroup, new: PrecedenceGroup) + + /// The named precedence group is missing from the precedence graph. + case missingGroup(PrecedenceGroupName, referencedFrom: Syntax) + + /// Error produced when a given operator already exists. + case operatorAlreadyExists(existing: Operator, new: Operator) + + /// The named operator is missing from the precedence graph. + case missingOperator(OperatorName, referencedFrom: Syntax) + + /// No associativity relationship between operators. + case incomparableOperators( + leftOperator: ExprSyntax, leftPrecedenceGroup: PrecedenceGroupName, + rightOperator: ExprSyntax, rightPrecedenceGroup: PrecedenceGroupName + ) +} + +/// A function that receives an operator precedence error and may do with it +/// whatever it likes. +/// +/// Operator precedence error handlers are passed into each function in the +/// operator-precedence parser that can produce a failure. The handler +/// may choose to throw (in which case the error will propagate outward) or +/// may separately record/drop the error and return without throwing (in +/// which case the operator-precedence parser will recover). +public typealias OperatorErrorHandler = + (OperatorError) throws -> Void diff --git a/Sources/SwiftOperators/OperatorTable+Defaults.swift b/Sources/SwiftOperators/OperatorTable+Defaults.swift new file mode 100644 index 00000000000..5a4a1aaa34b --- /dev/null +++ b/Sources/SwiftOperators/OperatorTable+Defaults.swift @@ -0,0 +1,406 @@ +//===------------------ OperatorPrecedence+Defaults.swift -----------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import SwiftSyntax + +/// Prefabricated operator precedence graphs. +extension OperatorTable { + /// Operator precedence graph for the logical operators '&&' and '||', for + /// example as it is used in `#if` processing. + public static var logicalOperators: OperatorTable { + let precedenceGroups: [PrecedenceGroup] = [ + PrecedenceGroup(name: "LogicalConjunctionPrecedence", + associativity: .left, assignment: false, + relations: [.higherThan("LogicalDisjunctionPrecedence")]), + PrecedenceGroup(name: "LogicalDisjunctionPrecedence", + associativity: .left, assignment: false, + relations: []) + ] + + let operators: [Operator] = [ + Operator(kind: .infix, name: "&&", + precedenceGroup: "LogicalConjunctionPrecedence"), + Operator(kind: .infix, name: "||", + precedenceGroup: "LogicalDisjunctionPrecedence") + ] + + return try! OperatorTable( + precedenceGroups: precedenceGroups, operators: operators) + } + + /// Operator precedence graph for the Swift standard library. + /// + /// This describes the operators within the Swift standard library at the + /// type of this writing. It can be used to approximate the behavior one + /// would get from parsing the actual Swift standard library's operators + /// without requiring access to the standard library source code. However, + /// because it does not incorporate user-defined operators, it will only + /// ever be useful for a quick approximation. + public static var standardOperators: OperatorTable { + let precedenceGroups: [PrecedenceGroup] = [ + PrecedenceGroup( + name: "AssignmentPrecedence", + associativity: .right, + assignment: true + ), + + PrecedenceGroup( + name: "FunctionArrowPrecedence", + associativity: .right, + relations: [.higherThan("AssignmentPrecedence")] + ), + + PrecedenceGroup( + name: "TernaryPrecedence", + associativity: .right, + relations: [.higherThan("FunctionArrowPrecedence")] + ), + + PrecedenceGroup( + name: "DefaultPrecedence", + relations: [.higherThan("TernaryPrecedence")] + ), + + PrecedenceGroup( + name: "LogicalDisjunctionPrecedence", + associativity: .left, + relations: [.higherThan("TernaryPrecedence")] + ), + + PrecedenceGroup( + name: "LogicalConjunctionPrecedence", + associativity: .left, + relations: [.higherThan("LogicalDisjunctionPrecedence")] + ), + + PrecedenceGroup( + name: "ComparisonPrecedence", + relations: [.higherThan("LogicalConjunctionPrecedence")] + ), + + PrecedenceGroup( + name: "NilCoalescingPrecedence", + associativity: .right, + relations: [.higherThan("ComparisonPrecedence")] + ), + + PrecedenceGroup( + name: "CastingPrecedence", + relations: [.higherThan("NilCoalescingPrecedence")] + ), + + PrecedenceGroup( + name: "RangeFormationPrecedence", + relations: [.higherThan("CastingPrecedence")] + ), + + PrecedenceGroup( + name: "AdditionPrecedence", + associativity: .left, + relations: [.higherThan("RangeFormationPrecedence")] + ), + + PrecedenceGroup( + name: "MultiplicationPrecedence", + associativity: .left, + relations: [.higherThan("AdditionPrecedence")] + ), + + PrecedenceGroup( + name: "BitwiseShiftPrecedence", + relations: [.higherThan("MultiplicationPrecedence")] + ) + ] + + let operators: [Operator] = [ + // "Exponentiative" + Operator( + kind: .infix, + name: "<<", + precedenceGroup: "BitwiseShiftPrecedence" + ), + + Operator( + kind: .infix, + name: "&<<", + precedenceGroup: "BitwiseShiftPrecedence" + ), + + Operator( + kind: .infix, + name: ">>", + precedenceGroup: "BitwiseShiftPrecedence" + ), + + Operator( + kind: .infix, + name: "&>>", + precedenceGroup: "BitwiseShiftPrecedence" + ), + + // "Multiplicative" + Operator( + kind: .infix, + name: "*", + precedenceGroup: "MultiplicationPrecedence" + ), + + Operator( + kind: .infix, + name: "&*", + precedenceGroup: "MultiplicationPrecedence" + ), + + Operator( + kind: .infix, + name: "/", + precedenceGroup: "MultiplicationPrecedence" + ), + + Operator( + kind: .infix, + name: "%", + precedenceGroup: "MultiplicationPrecedence" + ), + + Operator( + kind: .infix, + name: "&", + precedenceGroup: "MultiplicationPrecedence" + ), + + // "Additive" + Operator( + kind: .infix, + name: "+", + precedenceGroup: "AdditionPrecedence" + ), + + Operator( + kind: .infix, + name: "&+", + precedenceGroup: "AdditionPrecedence" + ), + + Operator( + kind: .infix, + name: "-", + precedenceGroup: "AdditionPrecedence" + ), + + Operator( + kind: .infix, + name: "&-", + precedenceGroup: "AdditionPrecedence" + ), + + Operator( + kind: .infix, + name: "|", + precedenceGroup: "AdditionPrecedence" + ), + + Operator( + kind: .infix, + name: "^", + precedenceGroup: "AdditionPrecedence" + ), + + Operator( + kind: .infix, + name: "...", + precedenceGroup: "RangeFormationPrecedence" + ), + + Operator( + kind: .infix, + name: "..<", + precedenceGroup: "RangeFormationPrecedence" + ), + + // "Coalescing" + Operator( + kind: .infix, + name: "??", + precedenceGroup: "NilCoalescingPrecedence" + ), + + // "Comparative" + Operator( + kind: .infix, + name: "<", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: "<=", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: ">", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: ">=", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: "==", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: "!=", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: "===", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: "!==", + precedenceGroup: "ComparisonPrecedence" + ), + + Operator( + kind: .infix, + name: "~=", + precedenceGroup: "ComparisonPrecedence" + ), + + // "Conjunctive" + Operator( + kind: .infix, + name: "&&", + precedenceGroup: "LogicalConjunctionPrecedence" + ), + + // "Disjunctive" + Operator( + kind: .infix, + name: "||", + precedenceGroup: "LogicalDisjunctionPrecedence" + ), + + + Operator( + kind: .infix, + name: "*=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "&*=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "/=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "%=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "+=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "&+=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "-=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "&-=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "<<=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "&<<=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: ">>=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "&>>=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "&=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "^=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "|=", + precedenceGroup: "AssignmentPrecedence" + ), + + Operator( + kind: .infix, + name: "~>", + precedenceGroup: nil + ) + ] + + return try! OperatorTable( + precedenceGroups: precedenceGroups, operators: operators) + } +} diff --git a/Sources/SwiftOperators/OperatorTable+Folding.swift b/Sources/SwiftOperators/OperatorTable+Folding.swift new file mode 100644 index 00000000000..e25bd602142 --- /dev/null +++ b/Sources/SwiftOperators/OperatorTable+Folding.swift @@ -0,0 +1,502 @@ +//===------------------ OperatorPrecedence+Folding.swift ------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import SwiftSyntax + +extension ExprSyntax { + // Is this an unresolved explicit cast? + fileprivate var isUnresolvedExplicitCast: Bool { + self.is(UnresolvedIsExprSyntax.self) || self.is(UnresolvedAsExprSyntax.self) + } +} + +extension OperatorTable { + private struct PrecedenceBound { + let groupName: PrecedenceGroupName? + let isStrict: Bool + let syntax: Syntax? + } + + /// Determine whether we should consider an operator in the given group + /// based on the specified bound. + private func shouldConsiderOperator( + fromGroup groupName: PrecedenceGroupName?, + in bound: PrecedenceBound, + operatorSyntax: Syntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> Bool { + guard let boundGroupName = bound.groupName else { + return true + } + + guard let groupName = groupName else { + return false + } + + if groupName == boundGroupName { + return !bound.isStrict + } + + return try precedenceGraph.precedence( + relating: groupName, to: boundGroupName, + startSyntax: operatorSyntax, endSyntax: bound.syntax!, + errorHandler: errorHandler + ) != .lowerThan + } + + /// Look up the precedence group for the given expression syntax. + private func lookupPrecedence( + of expr: ExprSyntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> PrecedenceGroupName? { + // A binary operator. + if let binaryExpr = expr.as(BinaryOperatorExprSyntax.self) { + let operatorName = binaryExpr.operatorToken.text + return try lookupOperatorPrecedenceGroupName( + operatorName, referencedFrom: Syntax(binaryExpr.operatorToken), + errorHandler: errorHandler + ) + } + + // The ternary operator has a fixed precedence group name. + if expr.is(UnresolvedTernaryExprSyntax.self) { + return "TernaryPrecedence" + } + + // An assignment operator has fixed precedence. + if expr.is(AssignmentExprSyntax.self) { + return "AssignmentPrecedence" + } + + // Cast operators have fixed precedence. + if expr.isUnresolvedExplicitCast { + return "CastingPrecedence" + } + + // The arrow operator has fixed precedence. + if expr.is(ArrowExprSyntax.self) { + return "FunctionArrowPrecedence" + } + + return nil + } + + /// Form a binary operation expression for, e.g., a + b. + @_spi(Testing) + public static func makeBinaryOperationExpr( + lhs: ExprSyntax, op: ExprSyntax, rhs: ExprSyntax + ) -> ExprSyntax { + // If the left-hand side is a "try" or "await", hoist it up to encompass + // the right-hand side as well. + if let tryExpr = lhs.as(TryExprSyntax.self) { + return ExprSyntax( + TryExprSyntax( + tryExpr.unexpectedBeforeTryKeyword, + tryKeyword: tryExpr.tryKeyword, + tryExpr.unexpectedBetweenTryKeywordAndQuestionOrExclamationMark, + questionOrExclamationMark: tryExpr.questionOrExclamationMark, + tryExpr.unexpectedBetweenQuestionOrExclamationMarkAndExpression, + expression: makeBinaryOperationExpr( + lhs: tryExpr.expression, op: op, rhs: rhs) + ) + ) + } + + if let awaitExpr = lhs.as(AwaitExprSyntax.self) { + return ExprSyntax( + AwaitExprSyntax( + awaitExpr.unexpectedBeforeAwaitKeyword, + awaitKeyword: awaitExpr.awaitKeyword, + awaitExpr.unexpectedBetweenAwaitKeywordAndExpression, + expression: makeBinaryOperationExpr( + lhs: awaitExpr.expression, op: op, rhs: rhs) + ) + ) + } + + // The form of the binary operation depends on the operator itself, + // which will be one of the unresolved infix operators. + + // An operator such as '+'. + if let binaryOperatorExpr = op.as(BinaryOperatorExprSyntax.self) { + return ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: lhs, + operatorOperand: ExprSyntax(binaryOperatorExpr), + rightOperand: rhs) + ) + } + + // A ternary operator x ? y : z. + if let ternaryExpr = op.as(UnresolvedTernaryExprSyntax.self) { + return ExprSyntax( + TernaryExprSyntax( + conditionExpression: lhs, + ternaryExpr.unexpectedBeforeQuestionMark, + questionMark: ternaryExpr.questionMark, + ternaryExpr.unexpectedBetweenQuestionMarkAndFirstChoice, + firstChoice: ternaryExpr.firstChoice, + ternaryExpr.unexpectedBetweenFirstChoiceAndColonMark, + colonMark: ternaryExpr.colonMark, + secondChoice: rhs) + ) + } + + // An assignment operator x = y. + if let assignExpr = op.as(AssignmentExprSyntax.self) { + return ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: lhs, + operatorOperand: ExprSyntax(assignExpr), + rightOperand: rhs) + ) + } + + // An "is" type check. + if let isExpr = op.as(UnresolvedIsExprSyntax.self) { + // FIXME: Do we actually have a guarantee that the right-hand side is a + // type expression here? + return ExprSyntax( + IsExprSyntax( + expression: lhs, + isExpr.unexpectedBeforeIsTok, + isTok: isExpr.isTok, + typeName: rhs.as(TypeExprSyntax.self)!.type) + ) + } + + // An "as" cast. + if let asExpr = op.as(UnresolvedAsExprSyntax.self) { + // FIXME: Do we actually have a guarantee that the right-hand side is a + // type expression here? + return ExprSyntax( + AsExprSyntax( + expression: lhs, + asExpr.unexpectedBeforeAsTok, + asTok: asExpr.asTok, + asExpr.unexpectedBetweenAsTokAndQuestionOrExclamationMark, + questionOrExclamationMark: asExpr.questionOrExclamationMark, + typeName: rhs.as(TypeExprSyntax.self)!.type) + ) + } + + // An arrow expression (->). + if let arrowExpr = op.as(ArrowExprSyntax.self) { + return ExprSyntax( + InfixOperatorExprSyntax( + leftOperand: lhs, + operatorOperand: ExprSyntax(arrowExpr), + rightOperand: rhs) + ) + } + + // FIXME: Fallback that we should never need + fatalError("Unknown binary operator") + } + + /// Determine the associativity between two precedence groups. + private func associativity( + firstGroup: PrecedenceGroupName?, + firstOperatorSyntax: Syntax, + secondGroup: PrecedenceGroupName?, + secondOperatorSyntax: Syntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> Associativity { + guard let firstGroup = firstGroup, let secondGroup = secondGroup else { + return .none + } + + // If we have the same group, query its associativity. + if firstGroup == secondGroup { + guard let group = precedenceGraph.lookupGroup(firstGroup) else { + try errorHandler( + .missingGroup(firstGroup, referencedFrom: firstOperatorSyntax)) + return .none + } + + return group.associativity + } + + let prec = try precedenceGraph.precedence( + relating: firstGroup, to: secondGroup, + startSyntax: firstOperatorSyntax, endSyntax: secondOperatorSyntax, + errorHandler: errorHandler + ) + + switch prec { + case .higherThan: + return .left + + case .lowerThan: + return .right + + case .unrelated: + return .none + } + } + + /// "Fold" an expression sequence where the left-hand side has been broken + /// out and (potentially) folded somewhat, and the "rest" of the sequence is + /// consumed along the way + private func fold( + _ lhs: ExprSyntax, rest: inout Slice, + bound: PrecedenceBound, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> ExprSyntax { + if rest.isEmpty { return lhs } + + // We mutate the left-hand side in place as we fold the sequence. + var lhs = lhs + + /// Get the operator, if appropriate to this pass. + func getNextOperator() throws -> (ExprSyntax, PrecedenceGroupName?)? { + let op = rest.first! + + // If the operator's precedence is lower than the minimum, stop here. + let opPrecedence = try lookupPrecedence( + of: op, errorHandler: errorHandler) + if try !shouldConsiderOperator( + fromGroup: opPrecedence, in: bound, operatorSyntax: Syntax(op) + ) { + return nil + } + + return (op, opPrecedence) + } + + // Extract out the first operator. + guard var (op1, op1Precedence) = try getNextOperator() else { + return lhs + } + + // We will definitely be consuming at least one operator. + // Pull out the prospective RHS and slice off the first two elements. + rest = rest.dropFirst() + var rhs = rest.first! + rest = rest.dropFirst() + + while !rest.isEmpty { + // If the operator is a cast operator, the RHS can't extend past the type + // that's part of the cast production. + if op1.isUnresolvedExplicitCast { + lhs = Self.makeBinaryOperationExpr(lhs: lhs, op: op1, rhs: rhs) + guard let (newOp1, newOp1Precedence) = try getNextOperator() else { + return lhs + } + + op1 = newOp1 + op1Precedence = newOp1Precedence + + rest = rest.dropFirst() + rhs = rest.first! + rest = rest.dropFirst() + continue + } + + // Pull out the next binary operator. + let op2 = rest.first! + let op2Precedence = try lookupPrecedence( + of: op2, errorHandler: errorHandler) + + // If the second operator's precedence is lower than the + // precedence bound, break out of the loop. + if try !shouldConsiderOperator( + fromGroup: op2Precedence, in: bound, operatorSyntax: Syntax(op1), + errorHandler: errorHandler + ) { + break + } + + let associativity = try associativity( + firstGroup: op1Precedence, + firstOperatorSyntax: Syntax(op1), + secondGroup: op2Precedence, + secondOperatorSyntax: Syntax(op2), + errorHandler: errorHandler + ) + + switch associativity { + case .left: + // Apply left-associativity immediately by folding the first two + // operands. + lhs = Self.makeBinaryOperationExpr(lhs: lhs, op: op1, rhs: rhs) + op1 = op2 + op1Precedence = op2Precedence + rest = rest.dropFirst() + rhs = rest.first! + rest = rest.dropFirst() + + case .right where op1Precedence != op2Precedence: + // If the first operator's precedence is lower than the second + // operator's precedence, recursively fold all such + // higher-precedence operators starting from this point, then + // repeat. + rhs = try fold( + rhs, rest: &rest, + bound: PrecedenceBound( + groupName: op1Precedence, isStrict: true, syntax: Syntax(op1) + ), + errorHandler: errorHandler + ) + + case .right: + // Apply right-associativity by recursively folding operators + // starting from this point, then immediately folding the LHS and RHS. + rhs = try fold( + rhs, rest: &rest, + bound: PrecedenceBound( + groupName: op1Precedence, isStrict: false, syntax: Syntax(op1) + ), + errorHandler: errorHandler + ) + + lhs = Self.makeBinaryOperationExpr(lhs: lhs, op: op1, rhs: rhs) + + // If we've drained the entire sequence, we're done. + if rest.isEmpty { + return lhs + } + + // Otherwise, start all over with our new LHS. + return try fold( + lhs, rest: &rest, bound: bound, errorHandler: errorHandler + ) + + case .none: + // If we ended up here, it's because we're either: + // - missing precedence groups, + // - have unordered precedence groups, or + // - have the same precedence group with no associativity. + if let op1Precedence = op1Precedence, + let op2Precedence = op2Precedence { + try errorHandler( + .incomparableOperators( + leftOperator: op1, leftPrecedenceGroup: op1Precedence, + rightOperator: op2, rightPrecedenceGroup: op2Precedence + ) + ) + } + + // Recover by folding arbitrarily at this operator, then continuing. + lhs = Self.makeBinaryOperationExpr(lhs: lhs, op: op1, rhs: rhs) + return try fold(lhs, rest: &rest, bound: bound, errorHandler: errorHandler) + } + } + + // Fold LHS and RHS together and declare completion. + return Self.makeBinaryOperationExpr(lhs: lhs, op: op1, rhs: rhs) + } + + /// "Fold" a sequence expression into a structured syntax tree. + /// + /// A sequence expression results from parsing an expression involving + /// infix operators, such as `x + y * z`. Swift's grammar does not + /// involve operator precedence, so a sequence expression is a flat list + /// of all of the terms `x`, `+`, `y`, `*`, and `z`. This operation folds + /// a single sequence expression into a structured syntax tree that + /// represents the same source code, but describes the order of operations + /// as if the expression has been parenthesized `x + (y * z)`. + public func foldSingle( + _ sequence: SequenceExprSyntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> ExprSyntax { + let lhs = sequence.elements.first! + var rest = sequence.elements.dropFirst() + return try fold( + lhs, rest: &rest, + bound: PrecedenceBound(groupName: nil, isStrict: false, syntax: nil), + errorHandler: errorHandler + ) + } + + /// Syntax rewriter that folds all of the sequence expressions it + /// encounters. + private class SequenceFolder : SyntaxRewriter { + /// The first operator precedecence that caused the error handler to + /// also throw. + var firstFatalError: OperatorError? = nil + + let opPrecedence: OperatorTable + let errorHandler: OperatorErrorHandler + + init( + opPrecedence: OperatorTable, + errorHandler: @escaping OperatorErrorHandler + ) { + self.opPrecedence = opPrecedence + self.errorHandler = errorHandler + } + + override func visit(_ node: SequenceExprSyntax) -> ExprSyntax { + // If the error handler threw in response to an error, don't + // continue folding. + if firstFatalError != nil { + return ExprSyntax(node) + } + + let newNode = super.visit(node).as(SequenceExprSyntax.self)! + + // If the error handler threw in response to an error, don't + // continue folding. + if firstFatalError != nil { + return ExprSyntax(newNode) + } + + // Try to fold this sequence expression. + do { + return try opPrecedence.foldSingle(newNode) { origError in + do { + try errorHandler(origError) + } catch { + firstFatalError = origError + throw error + } + } + } catch { + return ExprSyntax(newNode) + } + } + } + + /// Fold all sequence expressions within the given syntax tree into a + /// structured syntax tree. + /// + /// This operation replaces all sequence expressions in the given syntax + /// tree with structured syntax trees, by walking the tree and invoking + /// `foldSingle` on each sequence expression it encounters. Use this to + /// provide structure to an entire tree. + /// + /// Due to the inability to express the implementation of this rethrowing + /// function, a throwing error handler will end up being called twice with + /// the first error that causes it to be thrown. The first call will stop + /// the operation, then the second must also throw. + public func foldAll( + _ node: Node, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> Syntax { + return try withoutActuallyEscaping(errorHandler) { errorHandler in + let folder = SequenceFolder( + opPrecedence: self, errorHandler: errorHandler + ) + let result = folder.visit(Syntax(node)) + + // If the sequence folder encountered an error that caused the error + // handler to throw, invoke the error handler again with the original + // error. + if let origFatalError = folder.firstFatalError { + try errorHandler(origFatalError) + fatalError("error handler did not throw again after \(origFatalError)") + } + + return result + } + } +} diff --git a/Sources/SwiftOperators/OperatorTable+Semantics.swift b/Sources/SwiftOperators/OperatorTable+Semantics.swift new file mode 100644 index 00000000000..b05fe4caeb4 --- /dev/null +++ b/Sources/SwiftOperators/OperatorTable+Semantics.swift @@ -0,0 +1,146 @@ +//===-------------- OperatorPrecedence+Semantics.swift --------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import SwiftSyntax + +extension PrecedenceGroup { + /// Form the semantic definition of a precedence group given its syntax. + /// + /// TODO: This ignores all semantic errors. + init(from syntax: PrecedenceGroupDeclSyntax) { + self.name = syntax.identifier.text + self.syntax = syntax + + for attr in syntax.groupAttributes { + switch attr.as(SyntaxEnum.self) { + // Relation (lowerThan, higherThan) + case .precedenceGroupRelation(let relation): + let isLowerThan = relation.higherThanOrLowerThan.text == "lowerThan" + for otherGroup in relation.otherNames { + let otherGroupName = otherGroup.name.text + let relationKind: PrecedenceRelation.Kind = isLowerThan ? .lowerThan + : .higherThan + let relation = PrecedenceRelation( + kind: relationKind, groupName: otherGroupName, syntax: otherGroup) + self.relations.append(relation) + } + + // Assignment + case .precedenceGroupAssignment(let assignment): + self.assignment = assignment.flag.text == "true" + + // Associativity + case .precedenceGroupAssociativity(let associativity): + switch associativity.value.text { + case "left": + self.associativity = .left + + case "right": + self.associativity = .right + + case "none": + self.associativity = .none + + default: + break + } + + default: + break + } + } + } +} + +extension Operator { + /// Form the semantic definition of an operator given its syntax. + /// + /// TODO: This ignores all semantic errors. + init(from syntax: OperatorDeclSyntax) { + self.syntax = syntax + + kind = syntax.modifiers?.compactMap { + OperatorKind(rawValue: $0.name.text) + }.first ?? .infix + + name = syntax.identifier.text + + precedenceGroup = syntax.operatorPrecedenceAndTypes?.precedenceGroup.text + } +} + +extension OperatorTable { + /// Integrate the operator and precedence group declarations from the given + /// source file into the operator precedence tables. + public mutating func addSourceFile( + _ sourceFile: SourceFileSyntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows { + class OperatorAndGroupVisitor : SyntaxAnyVisitor { + var opPrecedence: OperatorTable + var errors: [OperatorError] = [] + + init(opPrecedence: OperatorTable) { + self.opPrecedence = opPrecedence + super.init(viewMode: .fixedUp) + } + + private func errorHandler(error: OperatorError) { + errors.append(error) + } + + override func visit( + _ node: OperatorDeclSyntax + ) -> SyntaxVisitorContinueKind { + opPrecedence.record( + Operator(from: node), errorHandler: errorHandler) + return .skipChildren + } + + override func visit( + _ node: PrecedenceGroupDeclSyntax + ) -> SyntaxVisitorContinueKind { + opPrecedence.record( + PrecedenceGroup(from: node), errorHandler: errorHandler) + return .skipChildren + } + + // Only visit top-level entities to find operators and precedence groups. + override func visit( + _ node: SourceFileSyntax + ) -> SyntaxVisitorContinueKind { + return .visitChildren + } + + override func visit( + _ node: CodeBlockItemListSyntax + ) -> SyntaxVisitorContinueKind { + return .visitChildren + } + + override func visit( + _ node: CodeBlockItemSyntax + ) -> SyntaxVisitorContinueKind { + return .visitChildren + } + + // Everything else stops the visitation. + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind{ + return .skipChildren + } + } + + let visitor = OperatorAndGroupVisitor(opPrecedence: self) + visitor.walk(sourceFile) + try visitor.errors.forEach(errorHandler) + self = visitor.opPrecedence + } +} diff --git a/Sources/SwiftOperators/OperatorTable.swift b/Sources/SwiftOperators/OperatorTable.swift new file mode 100644 index 00000000000..85888c6b638 --- /dev/null +++ b/Sources/SwiftOperators/OperatorTable.swift @@ -0,0 +1,128 @@ +//===------------------ OperatorPrecedence.swift --------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import SwiftSyntax + +/// Maintains and validates information about all operators in a Swift program. +/// +/// The operator table keep track of the various operator and precedence group +/// declarations within a program. Its core operations involve processing the +/// operator and precedence group declarations from a source tree into a +/// semantic representation, validating the correctness of those declarations, +/// and "folding" sequence expression syntax into a structured expression +/// syntax tree. +public struct OperatorTable { + var precedenceGraph: PrecedenceGraph = .init() + var infixOperators: [OperatorName : Operator] = [:] + var prefixOperators: [OperatorName : Operator] = [:] + var postfixOperators: [OperatorName : Operator] = [:] + + public init() { } + + /// Initialize the operator precedence instance with a given set of + /// operators and precedence groups. + @_optimize(none) + public init( + precedenceGroups: [PrecedenceGroup], + operators: [Operator], + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows { + for group in precedenceGroups { + try record(group, errorHandler: errorHandler) + } + for op in operators { + try record(op, errorHandler: errorHandler) + } + } + + /// Record the operator in the given operator array. + private func record( + _ op: Operator, + in table: inout [OperatorName : Operator], + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows { + if let existing = table[op.name] { + try errorHandler(.operatorAlreadyExists(existing: existing, new: op)) + } else { + table[op.name] = op + } + } + + /// Record the operator. + mutating func record( + _ op: Operator, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows { + switch op.kind { + case .infix: + return try record(op, in: &infixOperators, errorHandler: errorHandler) + + case .prefix: + return try record(op, in: &prefixOperators, errorHandler: errorHandler) + + case .postfix: + return try record(op, in: &postfixOperators, errorHandler: errorHandler) + } + } + + /// Record the precedence group. + mutating func record( + _ group: PrecedenceGroup, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows { + try precedenceGraph.add(group, errorHandler: errorHandler) + } +} + +extension OperatorTable { + /// Look for the precedence group corresponding to the given operator. + func lookupOperatorPrecedenceGroupName( + _ operatorName: OperatorName, + referencedFrom syntax: Syntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> PrecedenceGroupName? { + guard let op = infixOperators[operatorName] else { + try errorHandler( + .missingOperator(operatorName, referencedFrom: syntax)) + return nil + } + + return op.precedenceGroup + } +} + +extension OperatorTable: CustomStringConvertible { + /// The description of an operator table is the source code that produces it. + public var description: String { + var result = "" + + // Turn all of the dictionary values into their string representations. + func add( + _ dict: [Key : Value] + ) { + if dict.isEmpty { + return + } + + result.append(contentsOf: dict.sorted { $0.key < $1.key } + .map { $0.value.description } + .joined(separator: "\n")) + + result += "\n" + } + + add(precedenceGraph.precedenceGroups) + add(prefixOperators) + add(postfixOperators) + add(infixOperators) + return result + } +} diff --git a/Sources/SwiftOperators/PrecedenceGraph.swift b/Sources/SwiftOperators/PrecedenceGraph.swift new file mode 100644 index 00000000000..acb14936581 --- /dev/null +++ b/Sources/SwiftOperators/PrecedenceGraph.swift @@ -0,0 +1,156 @@ +// +// File.swift +// +// +// Created by Doug Gregor on 8/18/22. +// + +import SwiftSyntax + +/// Describes the relative precedence of two groups. +enum Precedence { + case unrelated + case higherThan + case lowerThan + + /// Flip the precedence order around. + var flipped: Precedence { + switch self { + case .unrelated: + return .unrelated + + case .higherThan: + return .lowerThan + + case .lowerThan: + return .higherThan + } + } +} + +/// A graph formed from a set of precedence groups, which can be used to +/// determine the relative precedence of two precedence groups. +struct PrecedenceGraph { + /// The known set of precedence groups, found by name. + var precedenceGroups: [PrecedenceGroupName : PrecedenceGroup] = [:] + + /// Add a new precedence group + /// + /// - throws: If there is already a precedence group with the given name, + /// throws PrecedenceGraphError.groupAlreadyExists. + mutating func add( + _ group: PrecedenceGroup, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows { + if let existing = precedenceGroups[group.name] { + try errorHandler( + OperatorError.groupAlreadyExists( + existing: existing, new: group)) + } else { + precedenceGroups[group.name] = group + } + } + + /// Look for the precedence group with the given name, or return nil if + /// no such group is known. + func lookupGroup(_ groupName: PrecedenceGroupName) -> PrecedenceGroup? { + return precedenceGroups[groupName] + } + + /// Search the precedence-group relationships, starting at the given + /// (fromGroup, fromSyntax) and following precedence groups in the + /// specified direction. + private func searchRelationships( + initialGroupName: PrecedenceGroupName, initialSyntax: Syntax, + targetGroupName: PrecedenceGroupName, + direction: PrecedenceRelation.Kind, + errorHandler: OperatorErrorHandler + ) rethrows -> Precedence? { + // Keep track of all of the groups we have seen during our exploration of + // the graph. This detects cycles and prevents extraneous work. + var groupsSeen: Set = [] + + var stack: [(PrecedenceGroupName, Syntax)] = + [(initialGroupName, initialSyntax)] + while let (currentGroupName, currentOperatorSyntax) = stack.popLast() { + guard let currentGroup = lookupGroup(currentGroupName) else { + try errorHandler( + .missingGroup(currentGroupName, referencedFrom: currentOperatorSyntax)) + continue + } + + for relation in currentGroup.relations { + if relation.kind == direction { + // If we hit our initial group, we're done. + let otherGroupName = relation.groupName + if otherGroupName == targetGroupName { + switch direction { + case .lowerThan: + return .lowerThan + + case .higherThan: + return .higherThan + } + } + + if groupsSeen.insert(otherGroupName).inserted { + let relationSyntax: Syntax + if let knownSyntax = relation.syntax { + relationSyntax = Syntax(knownSyntax) + } else { + relationSyntax = + Syntax(relation.synthesizedSyntax().otherNames.first!.name) + } + stack.append((otherGroupName, relationSyntax)) + } + } + } + } + + return nil + } + + /// Determine the precedence relationship between two precedence groups. + /// + /// Follow the precedence relationships among the precedence groups to + /// determine the precedence of the start group relative to the end group. + /// + /// - Returns: Precedence.lowerThan if startGroupName has lower precedence + /// than endGroupName, Precedence.higherThan if startGroupName has higher + /// precedence than endGroup name, and Precedence.unrelated otherwise. + func precedence( + relating startGroupName: PrecedenceGroupName, + to endGroupName: PrecedenceGroupName, + startSyntax: Syntax, + endSyntax: Syntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> Precedence { + if startGroupName == endGroupName { + return .unrelated + } + + // Walk all of the relationships from the end down, then from the beginning + // up, to determine whether there is a relation between the two groups. + return try searchRelationships( + initialGroupName: endGroupName, initialSyntax: endSyntax, + targetGroupName: startGroupName, direction: .lowerThan, + errorHandler: errorHandler + ) ?? searchRelationships( + initialGroupName: startGroupName, initialSyntax: startSyntax, + targetGroupName: endGroupName, direction: .higherThan, + errorHandler: errorHandler + ) ?? searchRelationships( + initialGroupName: startGroupName, initialSyntax: startSyntax, + targetGroupName: endGroupName, direction: .lowerThan, + errorHandler: errorHandler + ).map { + $0.flipped + } ?? searchRelationships( + initialGroupName: endGroupName, initialSyntax: endSyntax, + targetGroupName: startGroupName, direction: .higherThan, + errorHandler: errorHandler + ).map { + $0.flipped + } ?? .unrelated + } +} diff --git a/Sources/SwiftOperators/PrecedenceGroup.swift b/Sources/SwiftOperators/PrecedenceGroup.swift new file mode 100644 index 00000000000..ca503e2ccbf --- /dev/null +++ b/Sources/SwiftOperators/PrecedenceGroup.swift @@ -0,0 +1,127 @@ +//===------------------ PrecedenceGroup.swift -----------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import SwiftSyntax + +/// Names a precedence group. +/// +/// TODO: For now, we'll use strings, but we likely want to move this to +/// a general notion of an Identifier. +public typealias PrecedenceGroupName = String + +/// The associativity of a precedence group. +public enum Associativity: String { + /// The precedence group is nonassociative, meaning that one must + /// parenthesize when there are multiple operators in a sequence, e.g., + /// if ^ was nonassociative, a ^ b ^ c would need to be disambiguated as + /// either (a ^ b ) ^ c or a ^ (b ^ c). + case none + + /// The precedence group is left-associative, meaning that multiple operators + /// in the same sequence will be parenthesized from the left. This is typical + /// for arithmetic operators, such that a + b - c is treated as (a + b) - c. + case left + + /// The precedence group is right-associative, meaning that multiple operators + /// in the same sequence will be parenthesized from the right. This is used + /// for assignments, where a = b = c is treated as a = (b = c). + case right +} + +/// Describes the relationship of a precedence group to another precedence +/// group. +public struct PrecedenceRelation { + /// Describes the kind of a precedence relation. + public enum Kind { + case higherThan + case lowerThan + } + + /// The relationship to the other group. + public var kind: Kind + + /// The group name. + public var groupName: PrecedenceGroupName + + /// The syntax that provides the relation. This specifically refers to the + /// group name itself, but one can follow the parent pointer to find its + /// position. + public var syntax: PrecedenceGroupNameElementSyntax? + + /// Return a higher-than precedence relation. + public static func higherThan( + _ groupName: PrecedenceGroupName, + syntax: PrecedenceGroupNameElementSyntax? = nil + ) -> PrecedenceRelation { + return .init(kind: .higherThan, groupName: groupName, syntax: syntax) + } + + /// Return a lower-than precedence relation. + public static func lowerThan( + _ groupName: PrecedenceGroupName, + syntax: PrecedenceGroupNameElementSyntax? = nil + ) -> PrecedenceRelation { + return .init(kind: .lowerThan, groupName: groupName, syntax: syntax) + } +} + +/// Precedence groups are used for parsing sequences of expressions in Swift +/// source code. Each precedence group defines the associativity of the +/// operator and its precedence relative to other precedence groups: +/// +/// precedencegroup MultiplicativePrecedence { +/// associativity: left +/// higherThan: AdditivePrecedence +/// } +/// +/// Operator declarations then specify which precedence group describes their +/// precedence, e.g., +/// +/// infix operator *: MultiplicationPrecedence +public struct PrecedenceGroup { + /// The name of the group, which must be unique. + public var name: PrecedenceGroupName + + /// The associativity for the group. + public var associativity: Associativity = .none + + /// Whether the operators in this precedence group are considered to be + /// assignment operators. + public var assignment: Bool = false + + /// The set of relations to other precedence groups that are defined by + /// this precedence group. + public var relations: [PrecedenceRelation] = [] + + /// The syntax node that describes this precedence group. + public var syntax: PrecedenceGroupDeclSyntax? = nil + + public init( + name: PrecedenceGroupName, + associativity: Associativity = .none, + assignment: Bool = false, + relations: [PrecedenceRelation] = [], + syntax: PrecedenceGroupDeclSyntax? = nil + ) { + self.name = name + self.associativity = associativity + self.assignment = assignment + self.relations = relations + self.syntax = syntax + } +} + +extension PrecedenceGroup: CustomStringConvertible { + /// The description of a precedence group is the source code that produces it. + public var description: String { + (syntax ?? synthesizedSyntax()).description + } +} diff --git a/Sources/SwiftOperators/SwiftOperators.docc/Info.plist b/Sources/SwiftOperators/SwiftOperators.docc/Info.plist new file mode 100644 index 00000000000..41dc5d9d9b7 --- /dev/null +++ b/Sources/SwiftOperators/SwiftOperators.docc/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleName + SwiftOperators + CFBundleDisplayName + SwiftOperators + CFBundleIdentifier + com.apple.swift-operators + CFBundleDevelopmentRegion + en + CFBundleIconFile + DocumentationIcon + CFBundleIconName + DocumentationIcon + CFBundlePackageType + DOCS + CFBundleShortVersionString + 0.1.0 + CDDefaultCodeListingLanguage + swift + CFBundleVersion + 0.1.0 + CDAppleDefaultAvailability + + SwiftOperators + + + name + macOS + version + 10.15 + + + + + diff --git a/Sources/SwiftOperators/SwiftOperators.docc/SwiftOperators.md b/Sources/SwiftOperators/SwiftOperators.docc/SwiftOperators.md new file mode 100644 index 00000000000..872d477537d --- /dev/null +++ b/Sources/SwiftOperators/SwiftOperators.docc/SwiftOperators.md @@ -0,0 +1,91 @@ +# ``SwiftOperators`` + + + +An implementation of Swift's user-defined operator declarations and precedence +groups, allowing a program to reason about the relative precedence of +infix operations and transform syntax trees to describe the order of operations. + + + +## Overview + +Swift allows developers to define new operators to use in expressions. For example, the infix `+` and `*` operators are defined by the Swift standard library with [operator declarations](https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#grammar_operator-declaration) that look like this: + +```swift +infix operator +: AdditionPrecedence +infix operator *: MultiplicationPrecedence +``` + +The associativity and relative precedence of these operators is defined via a [precedence group declaration](https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#grammar_precedence-group-declaration). For example, the precedence groups used for `+` and `*` are defined as follows: + +```swift +precedencegroup AdditionPrecedence { + associativity: left +} +precedencegroup MultiplicationPrecedence { + associativity: left + higherThan: AdditionPrecedence +} +``` + +The Swift parser itself does not reason about the semantics of operators or precedence groups. Instead, an expression such as `x + y * z` will be parsed into a `SequenceExprSyntax` node whose children are `x`, a `BinaryOperatorExprSyntax` node for `+`, `y`, a `BinaryOperatorExprSyntax` node for `*`, and `z`. This is all the structure that is possible to parse for a Swift program without semantic information about operators and precedence groups. + +The `SwiftOperators` module interprets operator and precedence group declarations to provide those semantics. Its primary operation is to "fold" a `SequenceExprSyntax` node into an equivalent syntax tree that fully expresses the order of operations: in our example case, this means that the resulting syntax node will be an `InfixOperatorExprSyntax` whose left-hand side is `x` and operator is `+`, and whose right-hand side is another `InfixOperatorExprSyntax` node representing `y * z`. The resulting syntax tree will still accurately represent the original source input, but will be completely describe the order of evaluation and be suitable for structured editing or later semantic passes, such as type checking. + + + +## Quickstart + +The `SwiftOperators` library is typically used to take a raw parse of Swift code and apply the operator-precedence transformation to it to replace all `SequenceExprSyntax` nodes with more structured syntax nodes. For example, we can use this library's representation of the Swift standard library operators to provide a structured syntax tree for the expression `x + y * z`: + +```swift +import SwiftSyntax +import SwiftParser +import SwiftOperators + +var opPrecedence = OperatorTable.standardOperators // Use the Swift standard library operators +let parsed = try Parser.parse(source: "x + y * z") +dump(parsed) // contains SequenceExprSyntax(x, +, y, *, z) +let folded = try opPrecedence.foldAll(parsed) +dump(folded) // contains InfixOperatorExpr(x, +, InfixOperatorExpr(y, *, z)) +``` + +The type maintains the table of known operators and precedence groups, and is the primary way in which one interacts with this library. The standard operators are provided as a static variable of this type, which will work to fold most Swift code, such as in the example above that folds `x + y * z`. + +If your Swift code involves operator and precedence group declarations, they can be parsed into another source file and then added to the `OperatorTable` instance using `addSourceFile`: + +```swift +let moreOperators = + """ + precedencegroup ExponentiationPrecedence { + associativity: right + higherThan: MultiplicationPrecedence + } + + infix operator **: ExponentiationPrecedence + """ +let parsedOperators = try Parser.parse(source: moreOperators) + +// Adds **, ExponentiationPrecedence to the set of known operators and precedence groups. +try opPrecedence.addSourceFile(parsedOperators) + +let parsed2 = try Parser.parse(source: "b ** c ** d") +dump(parsed2) // contains SequenceExprSyntax(b, **, c, **, d) +let folded2 = try opPrecedence.foldAll(parsed2) +dump(folded2) // contains InfixOperatorExpr(b, **, InfixOperatorExpr(c, **, d)) +``` + +## Error handling + +By default, any of the operations that can produce an error, whether folding a sequence or parsing a source file's operators and precedence groups into a table, will throw an instance of . However, each entry point takes an optional error handler (of type ) that will be called with each error that occurs. For example, we can capture errors like this: + +```swift +var errors: [OperatorError] = [] +let foldedExpr2e = opPrecedence.foldSingle(sequenceExpr2) { error in + errors.append(error) +} +``` + +As indicated by the lack of `try`, the folding operation will continue even in the presence of errors, and produce a structured syntax tree. That structured syntax tree will have had some fallback behavior applied (e.g., bias toward left-associative when operators cannot be compared), but the sequence expression(s) will have been replaced in the resulting tree. + diff --git a/Sources/SwiftOperators/SyntaxSynthesis.swift b/Sources/SwiftOperators/SyntaxSynthesis.swift new file mode 100644 index 00000000000..36d732cd765 --- /dev/null +++ b/Sources/SwiftOperators/SyntaxSynthesis.swift @@ -0,0 +1,136 @@ +//===------------------ SyntaxSynthesis.swift -----------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +extension Operator { + /// Synthesize a syntactic representation of this operator based on its + /// semantic definition. + public func synthesizedSyntax() -> OperatorDeclSyntax { + let modifiers = ModifierListSyntax( + [DeclModifierSyntax(name: .identifier("\(kind)"), detail: nil)] + ) + let operatorKeyword = TokenSyntax.operatorKeyword(leadingTrivia: .space) + let identifierSyntax = + TokenSyntax.identifier(name, leadingTrivia: .space) + let precedenceGroupSyntax = precedenceGroup.map { groupName in + OperatorPrecedenceAndTypesSyntax( + colon: .colonToken(), + precedenceGroup: .identifier(groupName, leadingTrivia: .space), + designatedTypes: DesignatedTypeListSyntax([]) + ) + } + + return OperatorDeclSyntax( + attributes: nil, modifiers: modifiers, operatorKeyword: operatorKeyword, + identifier: identifierSyntax, + operatorPrecedenceAndTypes: precedenceGroupSyntax + ) + } +} + +extension PrecedenceRelation { + /// Synthesize a syntactic representation of this precedence relation based on + /// its semantic definition. + /// + /// We only use this internally to synthesize syntactic locations. + func synthesizedSyntax( + indentation: Int = 4 + ) -> PrecedenceGroupRelationSyntax { + PrecedenceGroupRelationSyntax( + higherThanOrLowerThan: .contextualKeyword( + "\(kind)", + leadingTrivia: [ .newlines(1), .spaces(indentation) ] + ), + colon: .colonToken(), + otherNames: PrecedenceGroupNameListSyntax( + [ + PrecedenceGroupNameElementSyntax( + name: .identifier(groupName, leadingTrivia: .space), + trailingComma: nil) + ] + ) + ) + } +} + +extension PrecedenceGroup { + /// Synthesize a syntactic representation of this precedence group based on + /// its semantic definition. + public func synthesizedSyntax( + indentation: Int = 4 + ) -> PrecedenceGroupDeclSyntax { + let precedencegroupKeyword = TokenSyntax.precedencegroupKeyword() + let identifierSyntax = + TokenSyntax.identifier(name, leadingTrivia: .space) + let leftBrace = TokenSyntax.leftBraceToken(leadingTrivia: .space) + + var groupAttributes: [Syntax] = [] + + switch associativity { + case .left, .right: + groupAttributes.append( + Syntax( + PrecedenceGroupAssociativitySyntax( + associativityKeyword: + .identifier( + "associativity", + leadingTrivia: [ .newlines(1), .spaces(indentation) ] + ), + colon: .colonToken(), + value: .identifier("\(associativity)", leadingTrivia: .space) + ) + ) + ) + + case .none: + // None is the default associativity. + break + } + + if assignment { + groupAttributes.append( + Syntax( + PrecedenceGroupAssignmentSyntax( + assignmentKeyword: + .identifier( + "assignment", + leadingTrivia: [ .newlines(1), .spaces(indentation) ] + ), + colon: .colonToken(), + flag: .trueKeyword(leadingTrivia: .space) + ) + ) + ) + } + + for relation in relations { + groupAttributes.append( + Syntax( + relation.synthesizedSyntax() + ) + ) + } + + let rightBrace = TokenSyntax.rightBraceToken( + leadingTrivia: groupAttributes.isEmpty ? .space : .newline + ) + + return PrecedenceGroupDeclSyntax( + attributes: nil, modifiers: nil, + precedencegroupKeyword: precedencegroupKeyword, + identifier: identifierSyntax, leftBrace: leftBrace, + groupAttributes: PrecedenceGroupAttributeListSyntax(groupAttributes), + rightBrace: rightBrace + ) + } +} diff --git a/Sources/swift-parser-test/swift-parser-test.swift b/Sources/swift-parser-test/swift-parser-test.swift index 8ba95ab1074..d14f70250d1 100644 --- a/Sources/swift-parser-test/swift-parser-test.swift +++ b/Sources/swift-parser-test/swift-parser-test.swift @@ -14,6 +14,7 @@ import SwiftDiagnostics import SwiftSyntax import SwiftParser +import SwiftOperators import Foundation import ArgumentParser #if os(Windows) @@ -56,6 +57,19 @@ class SwiftParserTest: ParsableCommand { ) } +/// Fold all of the sequences in the given source file. +func foldAllSequences(_ tree: SourceFileSyntax) -> (Syntax, [Diagnostic]) { + var diags: [Diagnostic] = [] + + let recordOperatorError: (OperatorError) -> Void = { error in + diags.append(error.asDiagnostic) + } + var operatorTable = OperatorTable.standardOperators + operatorTable.addSourceFile(tree, errorHandler: recordOperatorError) + let resultTree = operatorTable.foldAll(tree, errorHandler: recordOperatorError) + return (resultTree, diags) +} + class VerifyRoundTrip: ParsableCommand { required init() {} @@ -74,6 +88,10 @@ class VerifyRoundTrip: ParsableCommand { @Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax") var enableBareSlashRegex: Bool? + @Flag(name: .long, + help: "Perform sequence folding with the standard operators") + var foldSequences: Bool = false + enum Error: Swift.Error, CustomStringConvertible { case roundTripFailed @@ -91,21 +109,30 @@ class VerifyRoundTrip: ParsableCommand { try source.withUnsafeBufferPointer { sourceBuffer in try Self.run( source: sourceBuffer, swiftVersion: swiftVersion, - enableBareSlashRegex: enableBareSlashRegex + enableBareSlashRegex: enableBareSlashRegex, + foldSequences: foldSequences ) } } static func run( source: UnsafeBufferPointer, swiftVersion: String?, - enableBareSlashRegex: Bool? + enableBareSlashRegex: Bool?, foldSequences: Bool ) throws { let tree = try Parser.parse( source: source, languageVersion: swiftVersion, enableBareSlashRegexLiteral: enableBareSlashRegex ) - if tree.syntaxTextBytes != [UInt8](source) { + + let resultTree: Syntax + if foldSequences { + resultTree = foldAllSequences(tree).0 + } else { + resultTree = Syntax(tree) + } + + if resultTree.syntaxTextBytes != [UInt8](source) { throw Error.roundTripFailed } } @@ -123,6 +150,10 @@ class PrintDiags: ParsableCommand { @Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax") var enableBareSlashRegex: Bool? + @Flag(name: .long, + help: "Perform sequence folding with the standard operators") + var foldSequences: Bool = false + func run() throws { let source = try getContentsOfSourceFile(at: sourceFile) @@ -132,7 +163,12 @@ class PrintDiags: ParsableCommand { languageVersion: swiftVersion, enableBareSlashRegexLiteral: enableBareSlashRegex ) - let diags = ParseDiagnosticsGenerator.diagnostics(for: tree) + var diags = ParseDiagnosticsGenerator.diagnostics(for: tree) + + if foldSequences { + diags += foldAllSequences(tree).1 + } + if diags.isEmpty { print("No diagnostics produced") } @@ -155,6 +191,10 @@ class DumpTree: ParsableCommand { @Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax") var enableBareSlashRegex: Bool? + @Flag(name: .long, + help: "Perform sequence folding with the standard operators") + var foldSequences: Bool = false + func run() throws { let source = try getContentsOfSourceFile(at: sourceFile) @@ -164,7 +204,15 @@ class DumpTree: ParsableCommand { languageVersion: swiftVersion, enableBareSlashRegexLiteral: enableBareSlashRegex ) - print(tree.recursiveDescription) + + let resultTree: Syntax + if foldSequences { + resultTree = foldAllSequences(tree).0 + } else { + resultTree = Syntax(tree) + } + + print(resultTree.recursiveDescription) } } } @@ -181,6 +229,10 @@ class Reduce: ParsableCommand { @Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax") var enableBareSlashRegex: Bool? + @Flag(name: .long, + help: "Perform sequence folding with the standard operators") + var foldSequences: Bool = false + @Flag(help: "Print status updates while reducing the test case") var verbose: Bool = false @@ -225,6 +277,10 @@ class Reduce: ParsableCommand { "--swift-version", swiftVersion ] } + if foldSequences { + process.arguments! += [ "--fold-sequences" ] + } + let sema = DispatchSemaphore(value: 0) process.standardOutput = FileHandle.nullDevice process.standardError = FileHandle.nullDevice @@ -257,7 +313,8 @@ class Reduce: ParsableCommand { private func runVerifyRoundTripInCurrentProcess(source: [UInt8]) throws -> Bool { do { try source.withUnsafeBufferPointer { sourceBuffer in - try VerifyRoundTrip.run(source: sourceBuffer, swiftVersion: self.swiftVersion, enableBareSlashRegex: self.enableBareSlashRegex) + try VerifyRoundTrip.run(source: sourceBuffer, swiftVersion: self.swiftVersion, enableBareSlashRegex: self.enableBareSlashRegex, + foldSequences: foldSequences) } } catch { return false diff --git a/Tests/SwiftOperatorsTest/OperatorTableTests.swift b/Tests/SwiftOperatorsTest/OperatorTableTests.swift new file mode 100644 index 00000000000..790fb69c699 --- /dev/null +++ b/Tests/SwiftOperatorsTest/OperatorTableTests.swift @@ -0,0 +1,391 @@ +//===------------------ OperatorTableTests.swift --------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import XCTest +import SwiftSyntax +import SwiftParser +@_spi(Testing) import SwiftOperators +import _SwiftSyntaxTestSupport + +/// Visitor that looks for ExprSequenceSyntax nodes. +private class ExprSequenceSearcher: SyntaxAnyVisitor { + var foundSequenceExpr = false + + override func visit( + _ node: SequenceExprSyntax + ) -> SyntaxVisitorContinueKind { + foundSequenceExpr = true + return .skipChildren + } + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind{ + return foundSequenceExpr ? .skipChildren : .visitChildren + } +} + +extension SyntaxProtocol { + /// Determine whether the given syntax contains an ExprSequence anywhere. + var containsExprSequence: Bool { + let searcher = ExprSequenceSearcher(viewMode: .sourceAccurate) + searcher.walk(self) + return searcher.foundSequenceExpr + } +} + +/// A syntax rewriter that folds explicitly-parenthesized sequence expressions +/// into a structured syntax tree. +class ExplicitParenFolder : SyntaxRewriter { + override func visit(_ node: TupleExprSyntax) -> ExprSyntax { + // Identify syntax nodes of the form (x (op) y), which is a + // TupleExprSyntax(SequenceExpr(x, (op), y)). + guard node.elementList.count == 1, + let firstNode = node.elementList.first, + firstNode.label == nil, + let sequenceExpr = firstNode.expression.as(SequenceExprSyntax.self), + sequenceExpr.elements.count == 3, + let leftOperand = sequenceExpr.elements.first, + let middleExpr = sequenceExpr.elements.removingFirst().first, + let rightOperand = + sequenceExpr.elements.removingFirst().removingFirst().first + else { + return ExprSyntax(node) + } + + return OperatorTable.makeBinaryOperationExpr( + lhs: visit(Syntax(leftOperand)).as(ExprSyntax.self)!, + op: visit(Syntax(middleExpr)).as(ExprSyntax.self)!, + rhs: visit(Syntax(rightOperand)).as(ExprSyntax.self)! + ) + } +} + +extension OperatorTable { + /// Assert that parsing and folding the given "unfolded" source code + /// produces the same syntax tree as the fully-parenthesized version of + /// the same source. + /// + /// The `expectedSource` should be a fully-parenthesized expression, e.g., + /// `(a + (b * c))` that expresses how the initial code should have been + /// folded. + func assertExpectedFold( + _ source: String, + _ fullyParenthesizedSource: String + ) throws { + // Parse and fold the source we're testing. + let parsed = try Parser.parse(source: source) + let foldedSyntax = try foldAll(parsed) + XCTAssertFalse(foldedSyntax.containsExprSequence) + + // Parse and "fold" the parenthesized version. + let parenthesizedParsed = try Parser.parse(source: fullyParenthesizedSource) + let parenthesizedSyntax = ExplicitParenFolder().visit(parenthesizedParsed) + XCTAssertFalse(parenthesizedSyntax.containsExprSequence) + + // Make sure the two have the same structure. + let subtreeMatcher = SubtreeMatcher(Syntax(foldedSyntax), markers: [:]) + do { + try subtreeMatcher.assertSameStructure(Syntax(parenthesizedSyntax)) + } catch { + XCTFail("Matching for a subtree failed with error: \(error)") + } + } +} + +public class OperatorPrecedenceTests: XCTestCase { + func testLogicalExprsSingle() throws { + let opPrecedence = OperatorTable.logicalOperators + let parsed = try Parser.parse(source: "x && y || w && v || z") + let sequenceExpr = + parsed.statements.first!.item.as(SequenceExprSyntax.self)! + let foldedExpr = try opPrecedence.foldSingle(sequenceExpr) + XCTAssertEqual("\(foldedExpr)", "x && y || w && v || z") + XCTAssertNil(foldedExpr.as(SequenceExprSyntax.self)) + } + + func testLogicalExprs() throws { + let opPrecedence = OperatorTable.logicalOperators + try opPrecedence.assertExpectedFold("x && y || w", "((x && y) || w)") + try opPrecedence.assertExpectedFold("x || y && w", "(x || (y && w))") + } + + func testSwiftExprs() throws { + let opPrecedence = OperatorTable.standardOperators + let parsed = try Parser.parse(source: "(x + y > 17) && x && y || w && v || z") + let sequenceExpr = + parsed.statements.first!.item.as(SequenceExprSyntax.self)! + let foldedExpr = try opPrecedence.foldSingle(sequenceExpr) + XCTAssertEqual("\(foldedExpr)", "(x + y > 17) && x && y || w && v || z") + XCTAssertNil(foldedExpr.as(SequenceExprSyntax.self)) + } + + func testNestedSwiftExprs() throws { + let opPrecedence = OperatorTable.standardOperators + let parsed = try Parser.parse(source: "(x + y > 17) && x && y || w && v || z") + let foldedAll = try opPrecedence.foldAll(parsed) + XCTAssertEqual("\(foldedAll)", "(x + y > 17) && x && y || w && v || z") + XCTAssertFalse(foldedAll.containsExprSequence) + } + + func testAssignExprs() throws { + let opPrecedence = OperatorTable.standardOperators + try opPrecedence.assertExpectedFold("a = b + c", "(a = (b + c))") + try opPrecedence.assertExpectedFold("a = b = c", "(a = (b = c))") + } + + func testCastExprs() throws { + let opPrecedence = OperatorTable.standardOperators + try opPrecedence.assertExpectedFold("a is (b)", "(a is (b))") + try opPrecedence.assertExpectedFold("a as c == nil", "((a as c) == nil)") + } + + func testArrowExpr() throws { + let opPrecedence = OperatorTable.standardOperators + try opPrecedence.assertExpectedFold( + "a = b -> c -> d", + "(a = (b -> (c -> d)))" + ) + } + + func testParsedLogicalExprs() throws { + let logicalOperatorSources = + """ + precedencegroup LogicalDisjunctionPrecedence { + associativity: left + } + + precedencegroup LogicalConjunctionPrecedence { + associativity: left + higherThan: LogicalDisjunctionPrecedence + } + + // "Conjunctive" + + infix operator &&: LogicalConjunctionPrecedence + + // "Disjunctive" + + infix operator ||: LogicalDisjunctionPrecedence + """ + + let parsedOperatorPrecedence = try Parser.parse(source: logicalOperatorSources) + var opPrecedence = OperatorTable() + try opPrecedence.addSourceFile(parsedOperatorPrecedence) + + let parsed = try Parser.parse(source: "x && y || w && v || z") + let sequenceExpr = + parsed.statements.first!.item.as(SequenceExprSyntax.self)! + let foldedExpr = try opPrecedence.foldSingle(sequenceExpr) + XCTAssertEqual("\(foldedExpr)", "x && y || w && v || z") + XCTAssertNil(foldedExpr.as(SequenceExprSyntax.self)) + } + + func testParseErrors() throws { + let sources = + """ + infix operator + + infix operator + + + precedencegroup A { + associativity: none + higherThan: B + } + + precedencegroup A { + associativity: none + higherThan: B + } + """ + + let parsedOperatorPrecedence = try Parser.parse(source: sources) + + var opPrecedence = OperatorTable() + var errors: [OperatorError] = [] + opPrecedence.addSourceFile(parsedOperatorPrecedence) { error in + errors.append(error) + } + + XCTAssertEqual(errors.count, 2) + guard case let .operatorAlreadyExists(existing, new) = errors[0] else { + XCTFail("expected an 'operator already exists' error") + return + } + + XCTAssertEqual(errors[0].message, "redefinition of infix operator '+'") + _ = existing + _ = new + + guard case let .groupAlreadyExists(existingGroup, newGroup) = errors[1] else { + XCTFail("expected a 'group already exists' error") + return + } + XCTAssertEqual(errors[1].message, "redefinition of precedence group 'A'") + _ = newGroup + _ = existingGroup + } + + func testUnaryErrors() throws { + let sources = + """ + prefix operator + + prefix operator + + + postfix operator - + prefix operator - + + postfix operator* + postfix operator* + """ + + let parsedOperatorPrecedence = try Parser.parse(source: sources) + + var opPrecedence = OperatorTable() + var errors: [OperatorError] = [] + opPrecedence.addSourceFile(parsedOperatorPrecedence) { error in + errors.append(error) + } + + XCTAssertEqual(errors.count, 2) + guard case let .operatorAlreadyExists(existing, new) = errors[0] else { + XCTFail("expected an 'operator already exists' error") + return + } + + XCTAssertEqual(errors[0].message, "redefinition of prefix operator '+'") + + XCTAssertEqual(errors[1].message, "redefinition of postfix operator '*'") + _ = existing + _ = new + } + + func testFoldErrors() throws { + let parsedOperatorPrecedence = try Parser.parse(source: + """ + precedencegroup A { + associativity: none + } + + precedencegroup C { + associativity: none + lowerThan: B + } + + precedencegroup D { + associativity: none + } + + infix operator +: A + infix operator -: A + + infix operator *: C + + infix operator ++: D + """) + + var opPrecedence = OperatorTable() + try opPrecedence.addSourceFile(parsedOperatorPrecedence) + + do { + var errors: [OperatorError] = [] + let parsed = try Parser.parse(source: "a + b * c") + let sequenceExpr = + parsed.statements.first!.item.as(SequenceExprSyntax.self)! + _ = opPrecedence.foldSingle(sequenceExpr) { error in + errors.append(error) + } + + XCTAssertEqual(errors.count, 2) + guard case let .missingGroup(groupName, location) = errors[0] else { + XCTFail("expected a 'missing group' error") + return + } + XCTAssertEqual(groupName, "B") + XCTAssertEqual(errors[0].message, "unknown precedence group 'B'") + _ = location + } + + do { + var errors: [OperatorError] = [] + let parsed = try Parser.parse(source: "a / c") + let sequenceExpr = + parsed.statements.first!.item.as(SequenceExprSyntax.self)! + _ = opPrecedence.foldSingle(sequenceExpr) { error in + errors.append(error) + } + + XCTAssertEqual(errors.count, 1) + guard case let .missingOperator(operatorName, location) = errors[0] else { + XCTFail("expected a 'missing operator' error") + return + } + XCTAssertEqual(operatorName, "/") + XCTAssertEqual(errors[0].message, "unknown infix operator '/'") + _ = location + } + + do { + var errors: [OperatorError] = [] + let parsed = try Parser.parse(source: "a + b - c") + let sequenceExpr = + parsed.statements.first!.item.as(SequenceExprSyntax.self)! + _ = opPrecedence.foldSingle(sequenceExpr) { error in + errors.append(error) + } + + XCTAssertEqual(errors.count, 1) + guard case let .incomparableOperators(_, leftGroup, _, rightGroup) = + errors[0] else { + XCTFail("expected an 'incomparable operator' error") + return + } + XCTAssertEqual(leftGroup, "A") + XCTAssertEqual(rightGroup, "A") + XCTAssertEqual( + errors[0].message, + "adjacent operators are in non-associative precedence group 'A'") + } + + do { + var errors: [OperatorError] = [] + let parsed = try Parser.parse(source: "a ++ b - d") + let sequenceExpr = + parsed.statements.first!.item.as(SequenceExprSyntax.self)! + _ = opPrecedence.foldSingle(sequenceExpr) { error in + errors.append(error) + } + + XCTAssertEqual(errors.count, 1) + guard case let .incomparableOperators(_, leftGroup, _, rightGroup) = + errors[0] else { + XCTFail("expected an 'incomparable operator' error") + return + } + XCTAssertEqual(leftGroup, "D") + XCTAssertEqual(rightGroup, "A") + XCTAssertEqual( + errors[0].message, + "adjacent operators are in unordered precedence groups 'D' and 'A'") + } + } + + func testTernaryExpr() throws { + let opPrecedence = OperatorTable.standardOperators + try opPrecedence.assertExpectedFold( + "b + c ? y : z ? z2 : z3", + "((b + c) ? y : (z ? z2 : z3))") + } + + func testTryAwait() throws { + let opPrecedence = OperatorTable.standardOperators + try opPrecedence.assertExpectedFold("try x + y", "try (x + y)") + try opPrecedence.assertExpectedFold( + "await x + y + z", "await ((x + y) + z)") + } +} diff --git a/Tests/SwiftOperatorsTest/SyntaxSynthesisTests.swift b/Tests/SwiftOperatorsTest/SyntaxSynthesisTests.swift new file mode 100644 index 00000000000..c6087a393d5 --- /dev/null +++ b/Tests/SwiftOperatorsTest/SyntaxSynthesisTests.swift @@ -0,0 +1,61 @@ +//===------------------ SyntaxSynthesisTests.swift ------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import XCTest +import SwiftSyntax +import SwiftOperators + +public class SyntaxSynthesisTests: XCTestCase { + func testInfixOperator() { + let plus = Operator( + kind: .infix, name: "+", precedenceGroup: "AdditivePrecedence") + let plusSyntax = plus.synthesizedSyntax() + XCTAssertEqual( + plusSyntax.description, "infix operator +: AdditivePrecedence") + } + + func testPrecedenceGroup() { + let group = PrecedenceGroup( + name: "MyGroup", associativity: .right, assignment: true, + relations: [ .lowerThan("BetterGroup"), .higherThan("WorseGroup")] + ) + let groupSyntax = group.synthesizedSyntax() + XCTAssertEqual( + groupSyntax.description, + """ + precedencegroup MyGroup { + associativity: right + assignment: true + lowerThan: BetterGroup + higherThan: WorseGroup + } + """) + } + + func testLogicalOperatorTable() { + let table = OperatorTable.logicalOperators + XCTAssertEqual( + table.description, + """ + precedencegroup LogicalConjunctionPrecedence { + associativity: left + higherThan: LogicalDisjunctionPrecedence + } + precedencegroup LogicalDisjunctionPrecedence { + associativity: left + } + infix operator &&: LogicalConjunctionPrecedence + infix operator ||: LogicalDisjunctionPrecedence + + """ + ) + } +}