From 70a57e5b1409754f9f43bd4b2aab410c06e8666c Mon Sep 17 00:00:00 2001 From: Devdatta Talele Date: Sat, 11 Jul 2026 12:35:00 +0530 Subject: [PATCH] [SwiftOperators] Expose precedence-group comparison on OperatorTable Add a public precedence(of:relativeTo:referencedFrom:errorHandler:) method to OperatorTable that reports the relationship between two precedence groups, and make the Precedence enum (and its `flipped` member) public so callers can consume the result. The underlying comparison already lived on the internal PrecedenceGraph.precedence(relating:to:startSyntax:endSyntax:) helper; this change simply lifts that capability to the public surface without altering the algorithm. Motivated by an existing use case in swift-format where clients want to check the precedence of an operator relative to another one (see issue #2258 and https://github.com/apple/swift-format/pull/647). Prior to this PR, callers had no supported way to obtain that information short of re-implementing the traversal. Tests exercise the new API in three regimes on the standard operator table: a direct higherThan/lowerThan relationship (MultiplicationPrecedence vs AdditionPrecedence), the same-group case, and an unrelated-groups case (DefaultPrecedence vs LogicalDisjunctionPrecedence, both of which sit above TernaryPrecedence with no other links between them). A separate test covers Precedence.flipped for all three cases. Resolves #2258. --- .../OperatorTable+Semantics.swift | 29 ++++++++++ Sources/SwiftOperators/PrecedenceGraph.swift | 4 +- .../OperatorTableTests.swift | 55 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftOperators/OperatorTable+Semantics.swift b/Sources/SwiftOperators/OperatorTable+Semantics.swift index 6bf67ca9e61..07c095a0699 100644 --- a/Sources/SwiftOperators/OperatorTable+Semantics.swift +++ b/Sources/SwiftOperators/OperatorTable+Semantics.swift @@ -155,4 +155,33 @@ extension OperatorTable { try visitor.errors.forEach(errorHandler) self = visitor.opPrecedence } + + /// Determine the precedence of one precedence group relative to another. + /// + /// - Parameters: + /// - firstGroupName: The precedence group whose position is being queried. + /// - secondGroupName: The precedence group being compared against. + /// - syntax: A syntax node used as the reference location when reporting + /// errors encountered while walking the precedence-group relationships. + /// - errorHandler: A handler invoked when an inconsistency is found in + /// the precedence-group relationships. By default the underlying + /// `OperatorError` is re-thrown. + /// - Returns: ``Precedence/higherThan`` if `firstGroupName` has higher + /// precedence than `secondGroupName`, ``Precedence/lowerThan`` if it has + /// lower precedence, and ``Precedence/unrelated`` otherwise (including + /// when both names refer to the same precedence group). + public func precedence( + of firstGroupName: PrecedenceGroupName, + relativeTo secondGroupName: PrecedenceGroupName, + referencedFrom syntax: Syntax, + errorHandler: OperatorErrorHandler = { throw $0 } + ) rethrows -> Precedence { + return try precedenceGraph.precedence( + relating: firstGroupName, + to: secondGroupName, + startSyntax: syntax, + endSyntax: syntax, + errorHandler: errorHandler + ) + } } diff --git a/Sources/SwiftOperators/PrecedenceGraph.swift b/Sources/SwiftOperators/PrecedenceGraph.swift index 4e8ab7daf8c..dcfae1ce91e 100644 --- a/Sources/SwiftOperators/PrecedenceGraph.swift +++ b/Sources/SwiftOperators/PrecedenceGraph.swift @@ -17,13 +17,13 @@ import SwiftSyntax #endif /// Describes the relative precedence of two groups. -enum Precedence: Sendable { +public enum Precedence: Sendable { case unrelated case higherThan case lowerThan /// Flip the precedence order around. - var flipped: Precedence { + public var flipped: Precedence { switch self { case .unrelated: return .unrelated diff --git a/Tests/SwiftOperatorsTest/OperatorTableTests.swift b/Tests/SwiftOperatorsTest/OperatorTableTests.swift index 47e5bd2ad2f..ac5b293ada8 100644 --- a/Tests/SwiftOperatorsTest/OperatorTableTests.swift +++ b/Tests/SwiftOperatorsTest/OperatorTableTests.swift @@ -441,4 +441,59 @@ class OperatorPrecedenceTests: XCTestCase { let folded = try OperatorTable.standardOperators.foldAll(original) XCTAssertEqual(original.description, folded.description) } + + func testPrecedenceGroupComparison() throws { + let opPrecedence = OperatorTable.standardOperators + let syntax = Syntax(ExprSyntax("x")) + + // MultiplicationPrecedence has "higherThan: AdditionPrecedence" in the + // standard operators, so comparing the two groups directly should return + // the corresponding relationship in both directions. + XCTAssertEqual( + try opPrecedence.precedence( + of: "MultiplicationPrecedence", + relativeTo: "AdditionPrecedence", + referencedFrom: syntax + ), + .higherThan + ) + XCTAssertEqual( + try opPrecedence.precedence( + of: "AdditionPrecedence", + relativeTo: "MultiplicationPrecedence", + referencedFrom: syntax + ), + .lowerThan + ) + + // Comparing a group to itself should always be reported as unrelated. + XCTAssertEqual( + try opPrecedence.precedence( + of: "AdditionPrecedence", + relativeTo: "AdditionPrecedence", + referencedFrom: syntax + ), + .unrelated + ) + + // Two groups that both sit above `TernaryPrecedence` but have no direct + // or transitive relationship to each other should be reported as unrelated. + // In the standard operator table, `DefaultPrecedence` and + // `LogicalDisjunctionPrecedence` are both declared `higherThan: + // TernaryPrecedence` with no other links between them. + XCTAssertEqual( + try opPrecedence.precedence( + of: "DefaultPrecedence", + relativeTo: "LogicalDisjunctionPrecedence", + referencedFrom: syntax + ), + .unrelated + ) + } + + func testPrecedenceGroupFlipped() { + XCTAssertEqual(Precedence.higherThan.flipped, .lowerThan) + XCTAssertEqual(Precedence.lowerThan.flipped, .higherThan) + XCTAssertEqual(Precedence.unrelated.flipped, .unrelated) + } }