Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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()
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great to have a test for this, because e.g. we could have an operator ++++ which we'd like to then turn into plusPlusPlusPlus perhaps, so this is a "consume one operator, get the name, continue" operation.

Picking the known ones is good! but notice that knownOperatorNames[String(character)] wouldn't work for "==": "isEqual", or other known ones that have more characters.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn’t know custom operators could have more than three characters, like ===========.

Thanks for noticing!


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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be worth changing into operatorKind: SwiftOperatorKind?`

and make a new SwiftOperatorKind that is enum SwiftOperatorKind { case prefixOperator; case binaryOperator; case postfixOperator }

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