From 6cf3f5d2bf477830e1fbf7a11c817c160557c761 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Sun, 12 Jul 2026 09:59:52 +0300 Subject: [PATCH 1/3] SwiftExtract: detect operator functions --- .../ExtractedDecls+JavaNaming.swift | 4 +- ...Swift2JavaGenerator+FunctionLowering.swift | 2 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- .../JavaIdentifierFactory.swift | 4 +- Sources/SwiftExtract/ExtractedDecls.swift | 2 + .../SwiftExtract/SwiftAnalysisVisitor.swift | 18 +- .../AnalysisResultTests.swift | 163 ++++++++++++++++++ 7 files changed, 188 insertions(+), 7 deletions(-) diff --git a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift index 68965ce14..492ff5d26 100644 --- a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift +++ b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift @@ -57,7 +57,7 @@ extension ExtractedFunc { package var javaGetterName: String? { switch apiKind { case .getter, .subscriptGetter: break - case .setter, .subscriptSetter, .function, .initializer, .enumCase: return nil + case .setter, .subscriptSetter, .function, .initializer, .enumCase, .`operator`: return nil } let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool @@ -81,7 +81,7 @@ extension ExtractedFunc { package var javaSetterName: String? { switch apiKind { case .setter, .subscriptSetter: break - case .getter, .subscriptGetter, .function, .initializer, .enumCase: return nil + case .getter, .subscriptGetter, .function, .initializer, .enumCase, .`operator`: return nil } let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index f1c50b0ea..9cf26d1d3 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -1030,7 +1030,7 @@ extension LoweredFunctionSignature { // Build the result. let resultExpr: ExprSyntax switch apiKind { - case .function, .initializer: + case .function, .initializer, .`operator`: let arguments = paramExprs.enumerated() .map { (i, argument) -> String in let argExpr = original.parameters[i].convention == .inout ? "&\(argument)" : argument diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 14e075b02..b8c84012a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -627,7 +627,7 @@ extension JNISwift2JavaGenerator { // Build the result let result: String switch decl.apiKind { - case .function, .initializer: + case .function, .initializer, .`operator`: let downcallArguments = zip( decl.functionSignature.parameters, arguments, diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index 823246305..ebeccc5a0 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -37,7 +37,7 @@ package struct JavaIdentifierFactory { switch method.apiKind { case .getter, .subscriptGetter: method.javaGetterName! case .setter, .subscriptSetter: method.javaSetterName! - case .function, .initializer, .enumCase: method.name + case .function, .initializer, .enumCase, .`operator`: method.name } methodsByBaseName[baseName, default: []].append(method) } @@ -68,7 +68,7 @@ package struct JavaIdentifierFactory { switch decl.apiKind { case .getter, .subscriptGetter: decl.javaGetterName! case .setter, .subscriptSetter: decl.javaSetterName! - case .function, .initializer, .enumCase: decl.name + case .function, .initializer, .enumCase, .`operator`: decl.name } var methodName = baseName + paramsSuffix(decl, baseName: baseName) if Self.javaKeywords.contains(methodName) { diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 580bddb47..d5490a2fe 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -25,6 +25,7 @@ public enum SwiftAPIKind: Equatable { case enumCase case subscriptGetter case subscriptSetter + case `operator` } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been @@ -343,6 +344,7 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { case .function, .initializer: "" case .subscriptGetter: "subscriptGetter:" case .subscriptSetter: "subscriptSetter:" + case .operator: "operator:" } let context = diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 76de54d70..730b5e21b 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -38,6 +38,15 @@ final class SwiftAnalysisVisitor { } private var deferredConstrainedExtensions: [DeferredConstrainedExtension] = [] + private static func isOperatorFunction(_ node: FunctionDeclSyntax) -> Bool { + switch node.name.tokenKind { + case .binaryOperator, .prefixOperator, .postfixOperator: + return true + default: + return false + } + } + func visit(inputFile: SwiftInputFile) { let node = inputFile.syntax for codeItem in node.statements { @@ -201,11 +210,18 @@ final class SwiftAnalysisVisitor { return } + let apiKind: SwiftAPIKind = + if typeContext != nil && Self.isOperatorFunction(node) { + .operator + } else { + .function + } + let extracted = ExtractedFunc( module: analyzer.swiftModuleName, swiftDecl: node, name: node.name.text.unescapedSwiftName, - apiKind: .function, + apiKind: apiKind, functionSignature: signature, ) diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 703e27ef6..081a0bf0b 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -174,6 +174,169 @@ struct AnalysisResultSuite { #expect(kinds == [.getter, .setter]) } + // ==== ----------------------------------------------------------------------- + // MARK: Operators + + @Test func operatortest() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Score { + public var value: Int + + public static func + (left: Score, right: Score) -> Score { + Score(value: left.value + right.value) + } + + public static func - (left: Score, right: Score) -> Score { + Score(value: left.value - right.value) + } + + public static func * (left: Score, right: Score) -> Score { + Score(value: left.value * right.value) + } + + public static func / (left: Score, right: Score) -> Score { + Score(value: left.value / right.value) + } + + public static func % (left: Score, right: Score) -> Score { + Score(value: left.value % right.value) + } + + public static func << (left: Score, right: Int) -> Score { + Score(value: left.value << right) + } + + public static func >> (left: Score, right: Int) -> Score { + Score(value: left.value >> right) + } + + public static func | (left: Score, right: Score) -> Score { + Score(value: left.value | right.value) + } + + public static prefix func ~ (score: Score) -> Score { + Score(value: ~score.value) + } + + public static prefix func - (score: Score) -> Score { + Score(value: -score.value) + } + + public static func == (left: Score, right: Score) -> Bool { + left.value == right.value + } + + public static func != (left: Score, right: Score) -> Bool { + left.value != right.value + } + + public static func < (left: Score, right: Score) -> Bool { + left.value < right.value + } + + public static func <= (left: Score, right: Score) -> Bool { + left.value <= right.value + } + + public static func > (left: Score, right: Score) -> Bool { + left.value > right.value + } + + public static func >= (left: Score, right: Score) -> Bool { + left.value >= right.value + } + + public static func & (left: Score, right: Score) -> Score { + Score(value: left.value & right.value) + } + + public static func ^ (left: Score, right: Score) -> Score { + Score(value: left.value ^ right.value) + } + + public static func ?? (left: Score?, right: Score) -> Score { + left ?? right + } + + public static prefix func ! (score: Score) -> Bool { + score.value == 0 + } + } + """ + ) + ], + moduleName: "Aquarium", + ) + + let score = try #require(result.extractedTypes["Score"]) + let plusOperator = try #require(score.methods.first { $0.name == "+" }) + let minusOperator = try #require(score.methods.first { $0.name == "-" }) + let timesOperator = try #require(score.methods.first { $0.name == "*" }) + let dividedByOperator = try #require(score.methods.first { $0.name == "/" }) + let remainderOperator = try #require(score.methods.first { $0.name == "%" }) + let shiftedLeftOperator = try #require(score.methods.first { $0.name == "<<" }) + let shiftedRightOperator = try #require(score.methods.first { $0.name == ">>" }) + let bitwiseOrOperator = try #require(score.methods.first { $0.name == "|" }) + let bitwiseNotOperator = try #require(score.methods.first { $0.name == "~" }) + let negatedOperator = try #require(score.methods.first { $0.name == "-"}) + let isEqualOperator = try #require(score.methods.first { $0.name == "==" }) + let isNotEqualOperator = try #require(score.methods.first { $0.name == "!=" }) + let lessThanOperator = try #require(score.methods.first { $0.name == "<" }) + let lessThanOrEqualOperator = try #require(score.methods.first { $0.name == "<=" }) + let greaterThanOperator = try #require(score.methods.first { $0.name == ">" }) + let greaterThanOrEqualOperator = try #require(score.methods.first { $0.name == ">=" }) + let bitwiseAndOperator = try #require(score.methods.first { $0.name == "&" }) + let bitwiseXorOperator = try #require(score.methods.first { $0.name == "^" }) + let coalescingNilOperator = try #require(score.methods.first { $0.name == "??" }) + let logicalNotOperator = try #require(score.methods.first { $0.name == "!" }) + + + #expect(plusOperator.name == "+") + #expect(plusOperator.apiKind == .operator) + #expect(minusOperator.name == "-") + #expect(minusOperator.apiKind == .operator) + #expect(timesOperator.name == "*") + #expect(timesOperator.apiKind == .operator) + #expect(dividedByOperator.name == "/") + #expect(dividedByOperator.apiKind == .operator) + #expect(remainderOperator.name == "%") + #expect(remainderOperator.apiKind == .operator) + #expect(shiftedLeftOperator.name == "<<") + #expect(shiftedLeftOperator.apiKind == .operator) + #expect(shiftedRightOperator.name == ">>") + #expect(shiftedRightOperator.apiKind == .operator) + #expect(bitwiseOrOperator.name == "|") + #expect(bitwiseOrOperator.apiKind == .operator) + #expect(bitwiseNotOperator.name == "~") + #expect(bitwiseNotOperator.apiKind == .operator) + #expect(negatedOperator.name == "-") + #expect(negatedOperator.apiKind == .operator) + #expect(isEqualOperator.name == "==") + #expect(isEqualOperator.apiKind == .operator) + #expect(isNotEqualOperator.name == "!=") + #expect(isNotEqualOperator.apiKind == .operator) + #expect(lessThanOperator.name == "<") + #expect(lessThanOperator.apiKind == .operator) + #expect(lessThanOrEqualOperator.name == "<=") + #expect(lessThanOrEqualOperator.apiKind == .operator) + #expect(greaterThanOperator.name == ">") + #expect(greaterThanOperator.apiKind == .operator) + #expect(greaterThanOrEqualOperator.name == ">=") + #expect(greaterThanOrEqualOperator.apiKind == .operator) + #expect(bitwiseAndOperator.name == "&") + #expect(bitwiseAndOperator.apiKind == .operator) + #expect(bitwiseXorOperator.name == "^") + #expect(bitwiseXorOperator.apiKind == .operator) + #expect(coalescingNilOperator.name == "??") + #expect(coalescingNilOperator.apiKind == .operator) + #expect(logicalNotOperator.name == "!") + #expect(logicalNotOperator.apiKind == .operator) + } + // ==== ----------------------------------------------------------------------- // MARK: Effect specifiers (throws / async) From 27481fd607945147ca234ad55b0657a29ec69237 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Mon, 13 Jul 2026 00:29:46 +0300 Subject: [PATCH 2/3] Implement operator translation to valid Java method names. --- .../JExtractSwiftLib/JavaExtractDecider.swift | 2 +- .../JavaIdentifierFactory.swift | 39 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 34e2e164a..a64ef4466 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -74,7 +74,7 @@ public struct JavaExtractDecider: ExtractDecider { // Swift operators have no Java mapping if let fn = decl.as(FunctionDeclSyntax.self) { switch fn.name.tokenKind { - case .binaryOperator, .prefixOperator, .postfixOperator: + case .prefixOperator, .postfixOperator: log.trace("Skip '\(decl.qualifiedNameForDebug)': operators are not supported on Java") return false default: diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index ebeccc5a0..f65816841 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -68,7 +68,8 @@ package struct JavaIdentifierFactory { switch decl.apiKind { case .getter, .subscriptGetter: decl.javaGetterName! case .setter, .subscriptSetter: decl.javaSetterName! - case .function, .initializer, .enumCase, .`operator`: decl.name + case .function, .initializer, .enumCase: decl.name + case .operator: Self.javaOperatorName(decl.name) } var methodName = baseName + paramsSuffix(decl, baseName: baseName) if Self.javaKeywords.contains(methodName) { @@ -94,6 +95,42 @@ package struct JavaIdentifierFactory { } } + private static func javaOperatorName(_ swiftName: String) -> String { + if let knownName = knownOperatorNames[swiftName] { + return knownName + } + + return swiftName.enumerated() + .map { index, character in + let name = knownOperatorNames[String(character)] ?? "operator" + return index == 0 ? name : name.firstCharacterUppercased + } + .joined() + } + + private static let knownOperatorNames: [String: String] = [ + "+": "plus", + "-": "minus", + "*": "times", + "/": "dividedBy", + "%": "remainder", + "<<": "shiftedLeft", + ">>": "shiftedRight", + "|": "bitwiseOr", + "~": "bitwiseNot", + "==": "isEqual", + "!=": "isNotEqual", + "<": "lessThan", + "<=": "lessThanOrEqual", + ">": "greaterThan", + ">=": "greaterThanOrEqual", + "&": "bitwiseAnd", + "^": "bitwiseXor", + "??": "coalescingNil", + "!": "logicalNot", + "=": "equal", + ] + static let javaKeywords: Set = [ /// https://docs.oracle.com/javase/specs/jls/se25/html/jls-3.html#jls-3.9 "abstract", "continue", "for", "new", "switch", From ccd2e0b61cd5f10d21679b0b367e85d0f7c4b6f2 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Mon, 13 Jul 2026 00:30:23 +0300 Subject: [PATCH 3/3] draft: function lowering --- .../Sources/MySwiftLibrary/Operators.swift | 37 +++++++++++++++++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 12 +++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Operators.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Operators.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Operators.swift new file mode 100644 index 000000000..3db77615b --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Operators.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public struct OperatorScore { + public var value: Int64 + + public init(value: Int64) { + self.value = value + } + + public static func + (left: OperatorScore, right: OperatorScore) -> OperatorScore { + OperatorScore(value: left.value + right.value) + } + + public static func - (left: OperatorScore, right: OperatorScore) -> OperatorScore { + OperatorScore(value: left.value - right.value) + } + + public static func * (left: OperatorScore, right: OperatorScore) -> OperatorScore { + OperatorScore(value: left.value * right.value) + } + + public static func / (left: OperatorScore, right: OperatorScore) -> OperatorScore { + OperatorScore(value: left.value / right.value) + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index b8c84012a..d5e3b058a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -627,7 +627,7 @@ extension JNISwift2JavaGenerator { // Build the result let result: String switch decl.apiKind { - case .function, .initializer, .`operator`: + case .function, .initializer: let downcallArguments = zip( decl.functionSignature.parameters, arguments, @@ -637,6 +637,16 @@ extension JNISwift2JavaGenerator { } .joined(separator: ", ") result = "\(tryClause)\(callee).\(decl.name)(\(downcallArguments))" + case .`operator`: + let downcallArguments: String = zip( + decl.functionSignature.parameters, + arguments, + ).map { originalParam, argument in + let label = originalParam.argumentLabel.map { "\($0): " } ?? "" + return "\(label)\(argument)" + } + .joined(separator: ", ") + result = "\(tryClause)\(callee).\(translatedDecl.name)(\(downcallArguments))" case .enumCase: let downcallArguments = zip(