Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

infix operator +-==*

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)
}

public static func +-==* (left: OperatorScore, right: OperatorScore) -> String {
"Called +-==* in Java successfully with left: \(left.value) and right: \(right.value)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

package com.example.swift;

import org.junit.jupiter.api.Test;
import org.swift.swiftkit.core.SwiftArena;

import static org.junit.jupiter.api.Assertions.*;

public class OperatorsTest {
@Test
void plus() {
try (var arena = SwiftArena.ofConfined()) {
var left = OperatorScore.init(40, arena);
var right = OperatorScore.init(2, arena);

var result = OperatorScore.plus(left, right, arena);

assertEquals(42, result.getValue());
}
}

@Test
void randomOperator() {
try (var arena = SwiftArena.ofConfined()) {
var left = OperatorScore.init(40, arena);
var right = OperatorScore.init(2, arena);

var result = OperatorScore.plusMinusIsEqualTimes(left, right);

assertEquals("Called +-==* in Java successfully with left: 40 and right: 2", result);
}
}
}
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]))"

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,15 @@ extension JNISwift2JavaGenerator {
}
.joined(separator: ", ")
result = "\(tryClause)\(callee).\(decl.name)(\(downcallArguments))"

case .binaryOperator:
precondition(arguments.count == 2, "Binary operator must have exactly 2 arguments: \(decl)")
result = "(\(tryClause)((\(arguments.first!)) \(decl.name) (\(arguments.last!))))"
case .prefixOperator:
precondition(arguments.count == 1, "Prefix operator must have exactly 1 argument: \(decl)")
result = "(\(tryClause)(\(decl.name) (\(arguments.first!))))"
case .postfixOperator:
precondition(arguments.count == 1, "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
58 changes: 57 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,60 @@ 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",
"&&": "and",
"||": "or",
".": "dot",
]

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
Loading
Loading