Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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.
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.
Resolved
switch node.name.tokenKind {
case .binaryOperator, .prefixOperator, .postfixOperator:
return true
default:
return false
}
}
Comment thread
ktoso marked this conversation as resolved.
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.
Resolved
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