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, .binaryOperator, .prefixOperator, .postfixOperator: 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, .binaryOperator, .prefixOperator, .postfixOperator: 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 @@ -1039,6 +1039,18 @@ extension LoweredFunctionSignature {
.joined(separator: ", ")
resultExpr = "\(callee)(\(raw: arguments))"

case .binaryOperator:
assert(paramExprs.count == 2)
resultExpr = "\(paramExprs[0]) \(callee) \(paramExprs[1])"
Comment thread
AbdAlRahmanGad marked this conversation as resolved.
Outdated

case .prefixOperator:
assert(paramExprs.count == 1)
resultExpr = "\(callee)\(paramExprs[0])"

case .postfixOperator:
assert(paramExprs.count == 1)
resultExpr = "\(paramExprs[0])\(callee)"

case .getter:
assert(paramExprs.isEmpty)
resultExpr = callee
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,21 @@ extension JNISwift2JavaGenerator {
}
.joined(separator: ", ")
result = "\(tryClause)\(callee).\(decl.name)(\(downcallArguments))"

case .binaryOperator:
guard arguments.count == 2 else {
Comment thread
AbdAlRahmanGad marked this conversation as resolved.
Outdated
fatalError("Binary operator must have exactly 2 arguments: \(decl)")
}
result = "(\(tryClause) ((\(arguments.first!)) \(decl.name) (\(arguments.last!))))"
case .prefixOperator:
guard arguments.count == 1 else {
fatalError("Prefix operator must have exactly 1 argument: \(decl)")
}
result = "(\(tryClause) (\(decl.name) (\(arguments.first!))))"
case .postfixOperator:
guard arguments.count == 1 else {
fatalError("Postfix operator must have exactly 1 argument: \(decl)")
}
result = "(\(tryClause) ((\(arguments.first!)) \(decl.name)))"
case .enumCase:
let downcallArguments = zip(
decl.functionSignature.parameters,
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
55 changes: 54 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, .binaryOperator, .prefixOperator, .postfixOperator: method.name
}
methodsByBaseName[baseName, default: []].append(method)
}
Expand Down Expand Up @@ -69,6 +69,8 @@ package struct JavaIdentifierFactory {
case .getter, .subscriptGetter: decl.javaGetterName!
case .setter, .subscriptSetter: decl.javaSetterName!
case .function, .initializer, .enumCase: decl.name
case .binaryOperator, .prefixOperator, .postfixOperator:
Self.javaOperatorName(decl.name)
Comment thread
AbdAlRahmanGad marked this conversation as resolved.
}
var methodName = baseName + paramsSuffix(decl, baseName: baseName)
if Self.javaKeywords.contains(methodName) {
Expand All @@ -94,6 +96,57 @@ package struct JavaIdentifierFactory {
}
}

private static func javaOperatorName(_ swiftName: String) -> String {
var index = swiftName.startIndex
var javaName = ""

while index < swiftName.endIndex {
let isFirstToken = index == swiftName.startIndex
let oneCharacterEndIndex = swiftName.index(after: index)
let oneCharacterToken = String(swiftName[index..<oneCharacterEndIndex])
var tokenEndIndex: String.Index = oneCharacterEndIndex
var tokenName = knownOperatorNames[oneCharacterToken] ?? "operator"

// Prefer a known two-character token if it exists, e.g. `==` instead of `=` + `=`.
if oneCharacterEndIndex < swiftName.endIndex {
let twoCharacterEndIndex = swiftName.index(after: oneCharacterEndIndex)
let twoCharacterToken = String(swiftName[index..<twoCharacterEndIndex])
if let knownName = knownOperatorNames[twoCharacterToken] {
tokenEndIndex = twoCharacterEndIndex
tokenName = knownName
}
}

javaName += isFirstToken ? tokenName : tokenName.firstCharacterUppercased
index = tokenEndIndex
}

return javaName
}
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
6 changes: 6 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public enum SwiftAPIKind: Equatable {
case enumCase
case subscriptGetter
case subscriptSetter
case prefixOperator
case binaryOperator
case postfixOperator
}

/// Describes a Swift nominal type (e.g., a class, struct, enum) that has been
Expand Down Expand Up @@ -343,6 +346,9 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible {
case .function, .initializer: ""
case .subscriptGetter: "subscriptGetter:"
case .subscriptSetter: "subscriptSetter:"
case .prefixOperator: "prefixOperator:"
case .binaryOperator: "binaryOperator:"
case .postfixOperator: "postfixOperator:"
}

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

private static func operatorKind(_ node: FunctionDeclSyntax) -> SwiftAPIKind? {
switch node.name.tokenKind {
case .binaryOperator:
return .binaryOperator
case .prefixOperator:
return .prefixOperator
case .postfixOperator:
return .postfixOperator
default:
return nil
}
}
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 +214,18 @@ final class SwiftAnalysisVisitor {
return
}

let apiKind: SwiftAPIKind =
if typeContext != nil && Self.operatorKind(node) != nil {
Self.operatorKind(node)!
} else {
.function
}

let extracted = ExtractedFunc(
module: analyzer.swiftModuleName,
swiftDecl: node,
name: node.name.text.unescapedSwiftName,
apiKind: .function,
apiKind: apiKind,
functionSignature: signature,
)

Expand Down
152 changes: 152 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIOperatorsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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
//
//===----------------------------------------------------------------------===//

import JExtractSwiftLib
import SwiftJavaConfigurationShared
import Testing

@Suite
struct JNIOperatorsTests {
let source =
"""
infix operator ==+
infix operator >=>=++
infix operator <<>>==+

public struct Number {
public static func + (left: Number, right: Number) -> Number {
Number()
}

public static func ==+ (left: Number, right: Number) -> Number {
Number()
}

public static func >=>=++ (left: Number, right: Number) -> Number {
Number()
}

public static func <<>>==+ (left: Number, right: Number) -> Number {
Number()
}
}
"""

@Test
func plus_javaBindings() throws {
try assertOutput(
input: source,
.jni,
.java,
expectedChunks: [
"""
/**
* Downcall to Swift:
* {@snippet lang=swift :
* public static func + (left: Number, right: Number) -> Number
* }
*/
public static Number plus(Number left, Number right, SwiftArena swiftArena) {
return Number.wrapMemoryAddressUnsafe(Number.$plus(left.$memoryAddress(), right.$memoryAddress()), swiftArena);
}
""",
"""
private static native long $plus(long left, long right);
""",
]
)
}

@Test
func isEqualPlus_javaBindings() throws {
try assertOutput(
input: source,
.jni,
.java,
detectChunkByInitialLines: 2,
expectedChunks: [
"""
public static Number isEqualPlus(Number left, Number right, SwiftArena swiftArena) {
return Number.wrapMemoryAddressUnsafe(Number.$isEqualPlus(left.$memoryAddress(), right.$memoryAddress()), swiftArena);
}
""",
"""
private static native long $isEqualPlus(long left, long right);
""",
]
)
}

@Test
func repeatedGreaterThanOrEqual_javaBindings() throws {
try assertOutput(
input: source,
.jni,
.java,
detectChunkByInitialLines: 2,
expectedChunks: [
"""
public static Number greaterThanOrEqualGreaterThanOrEqualPlusPlus(Number left, Number right, SwiftArena swiftArena) {
return Number.wrapMemoryAddressUnsafe(Number.$greaterThanOrEqualGreaterThanOrEqualPlusPlus(left.$memoryAddress(), right.$memoryAddress()), swiftArena);
}
""",
"""
private static native long $greaterThanOrEqualGreaterThanOrEqualPlusPlus(long left, long right);
""",
]
)
}

@Test
func mixedTwoCharacterTokens_javaBindings() throws {
try assertOutput(
input: source,
.jni,
.java,
detectChunkByInitialLines: 2,
expectedChunks: [
"""
public static Number shiftedLeftShiftedRightIsEqualPlus(Number left, Number right, SwiftArena swiftArena) {
return Number.wrapMemoryAddressUnsafe(Number.$shiftedLeftShiftedRightIsEqualPlus(left.$memoryAddress(), right.$memoryAddress()), swiftArena);
}
""",
"""
private static native long $shiftedLeftShiftedRightIsEqualPlus(long left, long right);
""",
]
)
}

@Test
func plus_swiftThunks() throws {
try assertOutput(
input: source,
.jni,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("Java_com_example_swift_Number__00024plus__JJ")
public func Java_com_example_swift_Number__00024plus__JJ(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, left: jlong, right: jlong) -> jlong {
...
let result$ = UnsafeMutablePointer<Number>.allocate(capacity: 1)
result$.initialize(to: ( ((left$.pointee) + (right$.pointee)))
Comment thread
ktoso marked this conversation as resolved.
Outdated
let resultBits$ = Int64(Int(bitPattern: result$))
return resultBits$.getJNILocalRefValue(in: environment)
}
"""
]
)
}
}
Comment thread
AbdAlRahmanGad marked this conversation as resolved.
Loading
Loading