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,35 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Closures over platform-width `Int` get their own functional interface.
public enum Arithmetic {
public static func apply(_ f: (Int) -> Int, to x: Int) -> Int {
f(x)
}

public static func combine(_ a: Int, _ b: Int, _ f: (Int, Int) -> Int) -> Int {
f(a, b)
}
}

/// Properties without a type annotation: the type is inferred from a literal or an initializer call.
public final class Settings {
public static let maximumCount = 42
public var isEnabled = false
public var title = "Untitled"
public var ratio = 0.5
public var featured = Item(id: "featured", count: 0)

public init() {}
}
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.assertFalse

class InferredTypesTest {
@Test
fun intClosures() {
assertEquals(10L, Arithmetic.apply({ it * 2 }, 5))
assertEquals(5L, Arithmetic.combine(2, 3) { a, b -> a + b })
}

@Test
fun unannotatedProperties() {
val settings = Settings.init()
assertEquals(42L, Settings.getMaximumCount())
assertFalse(settings.isEnabled)
assertEquals("Untitled", settings.title)
assertEquals(0.5, settings.ratio)
assertEquals("featured", settings.featured.id)

settings.title = "Renamed"
assertEquals("Renamed", settings.title)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,13 @@ public func closureMultipleArguments(
closure(input1, input2)
}
// snippet.end

/// Platform-width `Int` closures get their own functional interface.
public func applyIntClosure(_ f: (Int) -> Int, to x: Int) -> Int {
f(x)
}

public enum ClosureConstants {
/// No type annotation: inferred as `Int` from the literal.
public static let answer = 42
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,14 @@ void globalCallMeLongToDoubleFunction() {
double result = MySwiftLibrary.globalCallMeLongToDoubleFunction((long a) -> { return (double) a; });
assertEquals(1.0, result);
}

@Test
void intClosure() {
assertEquals(10, MySwiftLibrary.applyIntClosure(x -> x * 2, 5));
}

@Test
void unannotatedStaticLet() {
assertEquals(42, ClosureConstants.getAnswer());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -840,10 +840,12 @@ extension JNISwift2JavaGenerator {
return
}

// If contains one function type and it's known java functional interface type, we don't need to print the whole class
// If contains one function type and it's known java functional interface type, we don't need to print the whole class.
// Match on the Swift type, as the parameter translation does: `(Int) -> Int` has the Java shape of
// `LongUnaryOperator` but is translated to its own interface, which must then be printed.
if translated.functionTypes.count == 1,
let ty = translated.functionTypes.first,
KnownJavaFunctionalInterface.find(ty) != nil
KnownJavaFunctionalInterface.find(ty.swiftType) != nil
{
return
}
Expand All @@ -854,7 +856,7 @@ extension JNISwift2JavaGenerator {
"""
) { printer in
for functionType in translated.functionTypes {
if KnownJavaFunctionalInterface.find(functionType) != nil {
if KnownJavaFunctionalInterface.find(functionType.swiftType) != nil {
continue
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ extension JNISwift2JavaGenerator {
}

func translate(_ decl: ExtractedFunc) throws -> TranslatedFunctionDecl {
// Java has no `inout`; the thunk would pass a converted temporary, which doesn't compile.
if decl.functionSignature.parameters.contains(where: { $0.convention == .inout }) {
throw JavaTranslationError.inoutParametersNotSupported
}

let nativeTranslation = NativeJavaTranslation(
config: self.config,
javaPackage: self.javaPackage,
Expand Down Expand Up @@ -2263,6 +2268,9 @@ extension JNISwift2JavaGenerator {
/// a java class.
case wrappedJavaClassTranslationNotProvided(SwiftType)

/// `inout` parameters have no Java equivalent yet.
case inoutParametersNotSupported

// FIXME: Remove once we support protocol variables
case protocolVariablesNotSupported

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,9 @@ extension JNISwift2JavaGenerator {
}

let result = try translateClosureResult(functionType.resultType)
let knownInterface = KnownJavaFunctionalInterface.find(functionType)
let interfaceJavaType =
if let known = KnownJavaFunctionalInterface.find(functionType) {
if let known = knownInterface {
known.javaType
} else {
JavaType.class(
Expand All @@ -449,7 +450,10 @@ extension JNISwift2JavaGenerator {
],
conversion: .closureLowering(
parameters: parameters,
result: result
result: result,
// The method to call is decided with the interface, from the Swift type: the JNI
// signature alone can't tell `(Int) -> Int`'s own interface from `LongUnaryOperator`.
methodName: knownInterface?.method ?? "apply"
),
indirectConversion: nil,
conversionCheck: nil
Expand Down Expand Up @@ -733,6 +737,16 @@ extension JNISwift2JavaGenerator {
throw JavaTranslationError.unsupportedSwiftType(type)
}

// Platform-width integers are Java `long`s, but `Int.JNIType` is `jint` on 32-bit
// platforms: read the `long` as a 64-bit value, then narrow.
if let wideType = platformIntegerWideType(knownType) {
return NativeResult(
javaType: javaType,
conversion: .labelessInitializer(.initFromJNI(.placeholder, swiftType: wideType), swiftType: type),
outParameters: []
)
}

// Only support primitives for now.
return NativeResult(
javaType: javaType,
Expand All @@ -756,6 +770,15 @@ extension JNISwift2JavaGenerator {
}
}

/// `Int64`/`UInt64` for `Int`/`UInt`, whose JNI type is only 64-bit on 64-bit platforms.
private func platformIntegerWideType(_ knownType: SwiftKnownTypeDeclKind) -> SwiftType? {
switch knownType {
case .int: knownTypes.int64
case .uint: knownTypes.uint64
default: nil
}
}

func translateClosureParameter(
_ type: SwiftType,
parameterName: String
Expand All @@ -769,12 +792,21 @@ extension JNISwift2JavaGenerator {
throw JavaTranslationError.unsupportedSwiftType(type)
}

// Platform-width integers widen to 64 bits, so the `jvalue` holds the Java `long` the
// functional interface expects, also on 32-bit platforms.
let value: NativeSwiftConversionStep =
if let wideType = platformIntegerWideType(knownType) {
.labelessInitializer(.placeholder, swiftType: wideType)
} else {
.placeholder
}

// Only support primitives for now.
return NativeParameter(
parameters: [
JavaParameter(name: parameterName, type: javaType)
],
conversion: .getJValue(.placeholder),
conversion: .getJValue(value),
indirectConversion: nil,
conversionCheck: nil
)
Expand Down Expand Up @@ -1363,7 +1395,7 @@ extension JNISwift2JavaGenerator {
/// of the `Unsafe(Mutable)Pointer` types in Swift.
indirect case pointee(NativeSwiftConversionStep)

indirect case closureLowering(parameters: [NativeParameter], result: NativeResult)
indirect case closureLowering(parameters: [NativeParameter], result: NativeResult, methodName: String)

/// Escaping closure lowering using the protocol infrastructure.
/// This uses UpcallConversionStep for full support of optionals, arrays, custom types, etc.
Expand Down Expand Up @@ -1663,7 +1695,7 @@ extension JNISwift2JavaGenerator {
let inner = inner.render(&printer, placeholder)
return "\(inner).pointee"

case .closureLowering(let parameters, let nativeResult):
case .closureLowering(let parameters, let nativeResult, let methodName):
var printer = SwiftPrinter()

let methodSignature = MethodSignature(
Expand All @@ -1689,13 +1721,6 @@ extension JNISwift2JavaGenerator {
}


let methodName =
if let known = KnownJavaFunctionalInterface.find(methodSignature) {
known.method
} else {
"apply"
}

printer.print(
"""
let class$ = environment.interface.GetObjectClass(environment, \(placeholder))
Expand Down
57 changes: 55 additions & 2 deletions Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,53 @@ extension SwiftFunctionSignature {
return (parameters, effectSpecifiers, thrownTypedError)
}

/// The type of an unannotated variable, when its initializer makes it unambiguous without type
/// checking: a literal (`42`, `-1`, `1.5`, `"text"`, `true`), or an initializer call `T(...)` /
/// `A.B(...)` whose callee resolves to a nominal type. Anything else, such as `T.member` or
/// `makeValue()`, still needs an explicit annotation.
static func inferredType(of expr: ExprSyntax, lookupContext: SwiftTypeLookupContext) -> SwiftType? {
let knownTypes = SwiftKnownTypes(symbolTable: lookupContext.symbolTable)
if expr.is(IntegerLiteralExprSyntax.self) {
return knownTypes.int
}
if let prefix = expr.as(PrefixOperatorExprSyntax.self), prefix.operator.text == "-",
prefix.expression.is(IntegerLiteralExprSyntax.self)
{
return knownTypes.int
}
if expr.is(FloatLiteralExprSyntax.self) {
return knownTypes.double
}
if expr.is(StringLiteralExprSyntax.self) {
return knownTypes.string
}
if expr.is(BooleanLiteralExprSyntax.self) {
return knownTypes.bool
}
if let call = expr.as(FunctionCallExprSyntax.self),
let typeSyntax = typeSyntax(naming: call.calledExpression),
let type = try? SwiftType(typeSyntax, lookupContext: lookupContext),
case .nominal = type, !type.isUnresolvedTypePlaceholder
{
return type
}
return nil
}

/// `Foo` or `A.B` written as an expression, as a type.
private static func typeSyntax(naming expr: ExprSyntax) -> TypeSyntax? {
if let reference = expr.as(DeclReferenceExprSyntax.self), reference.argumentNames == nil {
return TypeSyntax(IdentifierTypeSyntax(name: reference.baseName.trimmed))
}
if let member = expr.as(MemberAccessExprSyntax.self), let base = member.base,
member.declName.argumentNames == nil,
let baseType = typeSyntax(naming: base)
{
return TypeSyntax(MemberTypeSyntax(baseType: baseType, name: member.declName.baseName.trimmed))
}
return nil
}

public init(
_ varNode: VariableDeclSyntax,
isSet: Bool,
Expand All @@ -369,10 +416,16 @@ extension SwiftFunctionSignature {
throw SwiftFunctionTranslationError.multipleBindings(varNode)
}

guard let varTypeNode = binding.typeAnnotation?.type else {
let valueType: SwiftType
if let varTypeNode = binding.typeAnnotation?.type {
valueType = try SwiftType(varTypeNode, lookupContext: lookupContext)
} else if let initializer = binding.initializer?.value,
let inferred = Self.inferredType(of: initializer, lookupContext: lookupContext)
{
valueType = inferred
} else {
throw SwiftFunctionTranslationError.missingTypeAnnotation(varNode)
}
let valueType = try SwiftType(varTypeNode, lookupContext: lookupContext)

var accessorEffects: AccessorEffects? = nil
switch binding.accessorBlock?.accessors {
Expand Down
Loading
Loading