Skip to content
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion Sources/JExtractSwiftLib/JavaExtractDecider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
39 changes: 38 additions & 1 deletion Sources/JExtractSwiftLib/JavaIdentifierFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -69,6 +69,7 @@ package struct JavaIdentifierFactory {
case .getter, .subscriptGetter: decl.javaGetterName!
case .setter, .subscriptSetter: decl.javaSetterName!
case .function, .initializer, .enumCase: decl.name
case .operator: Self.javaOperatorName(decl.name)
}
var methodName = baseName + paramsSuffix(decl, baseName: baseName)
if Self.javaKeywords.contains(methodName) {
Expand All @@ -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()
}
Comment thread
ktoso marked this conversation as resolved.

private static let knownOperatorNames: [String: String] = [
"+": "plus",
"-": "minus",
"*": "times",
"/": "dividedBy",
"%": "remainder",
"<<": "shiftedLeft",
">>": "shiftedRight",
"|": "bitwiseOr",
"~": "bitwiseNot",
"==": "isEqual",
"!=": "isNotEqual",
"<": "lessThan",
"<=": "lessThanOrEqual",
">": "greaterThan",
">=": "greaterThanOrEqual",
"&": "bitwiseAnd",
"^": "bitwiseXor",
Comment thread
AbdAlRahmanGad marked this conversation as resolved.
"??": "coalescingNil",
"!": "logicalNot",
"=": "equal",
]

static let javaKeywords: Set<String> = [
/// https://docs.oracle.com/javase/specs/jls/se25/html/jls-3.html#jls-3.9
"abstract", "continue", "for", "new", "switch",
Expand Down
2 changes: 2 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -343,6 +344,7 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible {
case .function, .initializer: ""
case .subscriptGetter: "subscriptGetter:"
case .subscriptSetter: "subscriptSetter:"
case .operator: "operator:"
}

let context =
Expand Down
18 changes: 17 additions & 1 deletion Sources/SwiftExtract/SwiftAnalysisVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ final class SwiftAnalysisVisitor {
}
private var deferredConstrainedExtensions: [DeferredConstrainedExtension] = []

private static func isOperatorFunction(_ node: FunctionDeclSyntax) -> Bool {
Comment thread
ktoso marked this conversation as resolved.
Outdated
switch node.name.tokenKind {
case .binaryOperator, .prefixOperator, .postfixOperator:
return true
default:
return false
}
}
Comment thread
ktoso marked this conversation as resolved.

func visit(inputFile: SwiftInputFile) {
let node = inputFile.syntax
for codeItem in node.statements {
Expand Down Expand Up @@ -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,
)

Expand Down
163 changes: 163 additions & 0 deletions Tests/SwiftExtractTests/AnalysisResultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,169 @@ struct AnalysisResultSuite {
#expect(kinds == [.getter, .setter])
}

// ==== -----------------------------------------------------------------------
// MARK: Operators

@Test func operatortest() throws {
Comment thread
AbdAlRahmanGad marked this conversation as resolved.
Outdated
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)

Expand Down
Loading