Skip to content
Open
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,42 @@
//===----------------------------------------------------------------------===//
//
// 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 enum Receipts {
/// Defaults the Kotlin caller can leave out, as it would with a Kotlin default.
public static func format(_ amount: Int64, currency: String = "USD", showsSign: Bool = false) -> String {
"\(showsSign && amount > 0 ? "+" : "")\(amount) \(currency)"
}
}

/// Callbacks with defaults, like a view model's: Kotlin passes both or neither.
public struct ReceiptPrinter: Sendable {
private let onPrinted: @Sendable (String) -> Void
private let onFailed: @Sendable () -> Void

public init(
onPrinted: @escaping @Sendable (String) -> Void = { _ in },
onFailed: @escaping @Sendable () -> Void = {}
) {
self.onPrinted = onPrinted
self.onFailed = onFailed
}

public func print(_ amount: Int64) {
if amount >= 0 {
onPrinted(Receipts.format(amount))
} else {
onFailed()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// 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.kotlin

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class DefaultArgumentsTest {
@Test
fun trailingDefaultsCanBeLeftOut() {
assertEquals("5 USD", Receipts.format(5))
assertEquals("5 EUR", Receipts.format(5, "EUR"))
assertEquals("+5 EUR", Receipts.format(5, "EUR", true))
}

@Test
fun defaultedCallbacks() {
val lines = mutableListOf<String>()
var failed = false
val printer = ReceiptPrinter.init({ lines += it }, { failed = true })
printer.print(5)
printer.print(-1)
assertEquals(listOf("5 USD"), lines)
assertTrue(failed)
ReceiptPrinter.init().print(5)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Trailing default arguments are Java overloads; Swift fills in the values left out.
public func greeting(name: String = "world", excited: Bool = false) -> String {
"Hello, \(name)\(excited ? "!" : ".")"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//===----------------------------------------------------------------------===//
//
// 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 static org.junit.jupiter.api.Assertions.*;

public class DefaultArgumentsTest {
@Test
void overloadsUseSwiftDefaults() {
assertEquals("Hello, world.", MySwiftLibrary.greeting());
assertEquals("Hello, Ana.", MySwiftLibrary.greeting("Ana"));
assertEquals("Hello, Ana!", MySwiftLibrary.greeting("Ana", true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ extension JNISwift2JavaGenerator {
_ printer: inout SwiftPrinter,
_ closureTy: SyntheticEscapingClosureFunctionType,
) {
guard printedEscapingClosureWrappers.insert(closureTy.javaInterfaceName).inserted else {
return
}
printer.printBraceBlock(
"""
@JavaInterface("\(closureTy.javaBinaryName)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -966,12 +966,19 @@ extension JNISwift2JavaGenerator {
{
return
}
// Overloads of a method share its helper class; the first, with every parameter, prints it.
guard printedJavaClosureHelperClasses.insert("\(translated.parentName).\(translated.name)").inserted else {
return
}

printer.printBraceBlock(
"""
public static class \(translated.name)
"""
) { printer in
// Only holds interfaces. Without a public constructor, Kotlin's `Type.method()` can't also mean
// `Type.method` constructed, when the method takes no arguments.
printer.print("private \(translated.name)() {}")
for functionType in translated.functionTypes {
if KnownJavaFunctionalInterface.find(functionType.swiftType) != nil {
continue
Expand Down
5 changes: 5 additions & 0 deletions Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {

/// Jextracted types that already have a minimal JavaKit class wrapper printed for closures.
var printedMinimalJavaClassWrappers: Set<String> = []
/// Overloads for default arguments share their closure parameters' wrappers and Java interfaces.
var printedEscapingClosureWrappers: Set<String> = []
var printedJavaClosureHelperClasses: Set<String> = []

/// Accumulates every ``@_cdecl`` symbol name emitted during thunk printing.
/// Written to a linker version script after generation when
Expand Down Expand Up @@ -141,6 +144,8 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
// Expand variadic functions into N overloads
var expandedAnalysis = analysis
expandedAnalysis.expandVariadicOverloads(maxOverloads: config.effectiveMaxVariadicOverloads)
// Default arguments become overloads without the trailing defaulted parameters
expandedAnalysis.expandDefaultArgumentOverloads()
self.analysis = expandedAnalysis

// Every extracted protocol that also gets a plain Java `interface`
Expand Down
19 changes: 19 additions & 0 deletions Sources/SwiftExtract/AnalysisResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ public struct AnalysisResult {
type.initializers = type.initializers.flatMap { $0.expandingVariadicOverloads(maxOverloads: maxOverloads) }
}
}

/// Adds an overload for each trailing defaulted parameter a function can be called without, unless
/// a declared function already has that name and parameter types. Protocol requirements can't have
/// default arguments, so protocols are left as they are.
public mutating func expandDefaultArgumentOverloads() {
self.extractedGlobalFuncs = Self.withDefaultArgumentOverloads(self.extractedGlobalFuncs)

for type in self.extractedTypes.values where type.swiftNominal.kind != .protocol {
type.methods = Self.withDefaultArgumentOverloads(type.methods)
type.initializers = Self.withDefaultArgumentOverloads(type.initializers)
}
}

private static func withDefaultArgumentOverloads(_ funcs: [ExtractedFunc]) -> [ExtractedFunc] {
var declared = Set(funcs.map(\.overloadKey))
return funcs.flatMap { function in
[function] + function.defaultArgumentOverloads().filter { declared.insert($0.overloadKey).inserted }
}
}
}

// ==== -----------------------------------------------------------------------
Expand Down
62 changes: 62 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,68 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible {

return overloads
}

/// Overloads that leave out this function's trailing defaulted parameters, longest first, for
/// callers without default arguments. Swift fills in the values they leave out.
///
/// A method whose result may need a `SwiftArena` also gets an overload ending in one, which a
/// Kotlin lambda converts to. So an overload is left out when the parameter after its last one is
/// a closure and the overload with that closure exists: `f(a, SwiftArena)` would be ambiguous with
/// `f(a, closure)`.
public func defaultArgumentOverloads() -> [ExtractedFunc] {
let parameters = functionSignature.parameters
let mayTakeArena = apiKind == .initializer || Self.resultMayNeedArena(functionSignature.result.type)
var overloads: [ExtractedFunc] = []
var count = parameters.count
var longerOverloadExists = true
while count > 0, parameters[count - 1].hasDefaultValue {
count -= 1
if mayTakeArena, longerOverloadExists, case .function = parameters[count].type {
longerOverloadExists = false
continue
}
longerOverloadExists = true
var signature = functionSignature
signature.parameters = Array(parameters.prefix(count))
overloads.append(
ExtractedFunc(
module: module,
swiftDecl: swiftDecl,
name: name,
apiKind: apiKind,
functionSignature: signature,
sourceFilePath: sourceFilePath
)
)
}
return overloads
}

/// Whether a result may be returned through a `SwiftArena`: anything but primitives, strings and
/// `Void`, or optionals and arrays of them.
private static func resultMayNeedArena(_ type: SwiftType) -> Bool {
switch type {
case .nominal(let nominal):
switch nominal.asKnownType {
case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double,
.string:
return false
case .optional(let wrapped), .array(let wrapped):
return resultMayNeedArena(wrapped)
default:
return true
}
case .tuple(let elements):
return elements.contains { resultMayNeedArena($0.type) }
default:
return true
}
}

/// Identifies an overload by name and parameter types, as Java does.
var overloadKey: String {
"\(name)(\(functionSignature.parameters.map { "\($0.type)" }.joined(separator: ",")))"
}
}

extension ExtractedFunc: Hashable {
Expand Down
1 change: 1 addition & 0 deletions Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,7 @@ struct JNIClosureTests {
expectedChunks: [
"""
public static class closureWithArgumentsAndReturn {
private closureWithArgumentsAndReturn() {}
/** Corresponds to the Swift closure parameter of type {@code (Int64, Bool) -> Int64}. */
@FunctionalInterface
public interface closure {
Expand Down
1 change: 1 addition & 0 deletions Tests/JExtractSwiftTests/JNI/JNICodegenFixesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct JNICodegenFixesTests {
expectedChunks: [
"""
public static class apply {
private apply() {}
/** Corresponds to the Swift closure parameter of type {@code (Int) -> Int}. */
@FunctionalInterface
public interface f {
Expand Down
Loading
Loading