Skip to content
Closed
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,72 @@
//===----------------------------------------------------------------------===//
//
// 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 func makeInt64Range() -> Range<Int64> {
-10..<20
}

public func int64Range(_ range: Range<Int64>) -> Range<Int64> {
range
}

public func platformIntRange(_ range: Range<Int>) -> Range<Int> {
range
}

public func uint64Range(_ range: Range<UInt64>) -> Range<UInt64> {
range
}

public func int32Range(_ range: Range<Int32>) -> Range<Int32> {
range
}

public func int64RangeContains(_ range: Range<Int64>, value: Int64) -> Bool {
range.contains(value)
}

public func acceptInt64Range(_ range: Range<Int64>) {
}

public func multipleRanges(_ first: Range<Int64>, _ second: Range<Int64>) -> Int64 {
first.lowerBound + second.lowerBound
}

public func doubleRange(_ range: Range<Double>) -> Range<Double> {
range
}

public func stringRange(_ range: Range<String>) -> Range<String> {
range
}

public func stringRangeIsEmpty(_ range: Range<String>) -> Bool {
range.isEmpty
}

public struct RangeBound: Comparable {
public let value: Int64

public init(value: Int64) {
self.value = value
}

public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.value < rhs.value
}
}

public func customBoundRange(_ range: Range<RangeBound>) -> Range<RangeBound> {
range
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//===----------------------------------------------------------------------===//
//
// 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 org.swift.swiftkit.core.SwiftRange;

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

public class SwiftRangeTest {
@Test
void returnRange() {
SwiftRange<Long> range = MySwiftLibrary.makeInt64Range();
assertEquals(-10L, range.getLowerBound());
assertEquals(20L, range.getUpperBound());
}

@Test
void roundTripAndHalfOpenBounds() {
SwiftRange<Long> input = new SwiftRange<>(-5L, 10L);
SwiftRange<Long> result = MySwiftLibrary.int64Range(input);
assertEquals(input.getLowerBound(), result.getLowerBound());
assertEquals(input.getUpperBound(), result.getUpperBound());
assertTrue(MySwiftLibrary.int64RangeContains(input, -5L));
assertFalse(MySwiftLibrary.int64RangeContains(input, 10L));
}

@Test
void emptyRange() {
SwiftRange<Long> range = MySwiftLibrary.int64Range(new SwiftRange<>(7L, 7L));
assertEquals(7L, range.getLowerBound());
assertEquals(7L, range.getUpperBound());
assertFalse(MySwiftLibrary.int64RangeContains(range, 7L));
}

@Test
void integerBounds() {
SwiftRange<Integer> range = MySwiftLibrary.int32Range(new SwiftRange<>(Integer.MIN_VALUE, Integer.MAX_VALUE));
assertEquals(Integer.MIN_VALUE, range.getLowerBound());
assertEquals(Integer.MAX_VALUE, range.getUpperBound());
}

@Test
void platformAndUnsignedIntegerBounds() {
SwiftRange<Long> signed = MySwiftLibrary.platformIntRange(new SwiftRange<>(-1L, 5L));
assertEquals(-1L, signed.getLowerBound());
assertEquals(5L, signed.getUpperBound());
// UInt64.max is represented by Java's signed long bit pattern -1.
SwiftRange<Long> unsigned = MySwiftLibrary.uint64Range(new SwiftRange<>(0L, -1L));
assertEquals(0L, unsigned.getLowerBound());
assertEquals(-1L, unsigned.getUpperBound());
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.uint64Range(new SwiftRange<>(-1L, 0L)));
}

@Test
void reversedBoundsThrowForObjectPrimitiveAndVoidReturns() {
SwiftRange<Long> invalid = new SwiftRange<>(10L, 5L);
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.int64Range(invalid));
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.int64RangeContains(invalid, 7L));
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.acceptInt64Range(invalid));
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.multipleRanges(invalid, invalid));
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.multipleRanges(new SwiftRange<>(0L, 1L), invalid));
assertEquals(-10L, MySwiftLibrary.makeInt64Range().getLowerBound());
}

@Test
void nullRangeAndNullBoundsThrow() {
assertThrows(NullPointerException.class, () -> MySwiftLibrary.int64Range(null));
assertThrows(NullPointerException.class, () -> new SwiftRange<Long>(null, 1L));
assertThrows(NullPointerException.class, () -> new SwiftRange<Long>(0L, null));
}

@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void erasedBoundTypesAreChecked() {
SwiftRange invalid = new SwiftRange<>("a", "z");
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.int64Range(invalid));
}

@Test
void floatingPointBounds() {
SwiftRange<Double> range = MySwiftLibrary.doubleRange(new SwiftRange<>(-0.5, 1.25));
assertEquals(-0.5, range.getLowerBound());
assertEquals(1.25, range.getUpperBound());
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.doubleRange(new SwiftRange<>(Double.NaN, 1.0)));
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.doubleRange(new SwiftRange<>(0.0, Double.NaN)));
}

@Test
void stringsUseSwiftOrdering() {
// Swift treats canonically equivalent strings as equal (an empty range),
// whereas Java compares their UTF-16 code units.
String lower = "\u00E9";
String upper = "e\u0301";
assertTrue(lower.compareTo(upper) > 0);
SwiftRange<String> result = MySwiftLibrary.stringRange(new SwiftRange<>(lower, upper));
assertEquals(lower, result.getLowerBound());
assertEquals(upper, result.getUpperBound());
assertTrue(MySwiftLibrary.stringRangeIsEmpty(result));
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.stringRange(new SwiftRange<>("z", "a")));
}

@Test
void swiftComparableBoundsDoNotRequireJavaComparable() {
try (var arena = SwiftArena.ofConfined()) {
RangeBound lower = RangeBound.init(1L, arena);
RangeBound upper = RangeBound.init(5L, arena);
SwiftRange<RangeBound> result = MySwiftLibrary.customBoundRange(new SwiftRange<>(lower, upper));
assertEquals(1L, result.getLowerBound().getValue());
assertEquals(5L, result.getUpperBound().getValue());
assertThrows(IllegalArgumentException.class, () -> MySwiftLibrary.customBoundRange(new SwiftRange<>(upper, lower)));
}
}
}
3 changes: 3 additions & 0 deletions Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func getJavaTypeAnnotations(swiftType: SwiftType, config: Configuration) -> [Jav
case .array(let element): // check recursively for [[UInt8]] etc
return getJavaTypeAnnotations(swiftType: element, config: config)

case .range(let bound):
return getJavaTypeAnnotations(swiftType: bound, config: config)

case .set(let element) where element.isUnsignedInteger:
return [JavaAnnotation.unsigned]
case .set(let element):
Expand Down
6 changes: 3 additions & 3 deletions Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ enum JNIJavaTypeTranslator {
.essentialsData, .essentialsDataProtocol,
.array,
.dictionary,
.set,
.set, .range,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
Expand Down Expand Up @@ -80,7 +80,7 @@ enum JNIJavaTypeTranslator {
.essentialsData, .essentialsDataProtocol,
.array,
.dictionary,
.set,
.set, .range,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
Expand Down Expand Up @@ -108,7 +108,7 @@ enum JNIJavaTypeTranslator {
.essentialsData, .essentialsDataProtocol,
.array,
.dictionary,
.set,
.set, .range,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ extension JNISwift2JavaGenerator {
case .nominal(let nominalType):
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
switch knownType {
case .optional, .array, .dictionary, .set:
case .optional, .array, .dictionary, .set, .range:
throw JavaTranslationError.unsupportedSwiftType(known: .array(elementType))
default:
break
Expand Down Expand Up @@ -350,7 +350,7 @@ extension JNISwift2JavaGenerator {
case .nominal(let nominalType):
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
switch knownType {
case .optional, .array, .dictionary, .set:
case .optional, .array, .dictionary, .set, .range:
throw JavaTranslationError.unsupportedSwiftType(known: .array(elementType))
default:
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,13 @@ extension JNISwift2JavaGenerator {
genericRequirements: genericRequirements,
)

case .range(let bound):
let boundJavaType = try translateGenericTypeParameter(bound, genericParameters: genericParameters, genericRequirements: genericRequirements)
return TranslatedParameter(
parameter: JavaParameter(name: parameterName, type: .swiftRange(boundJavaType)),
conversion: .requireNonNull(.placeholder, message: "\(parameterName) must not be null"),
)

case .set(let elementType):
return try translateSetParameter(
elementType: elementType,
Expand Down Expand Up @@ -970,6 +977,16 @@ extension JNISwift2JavaGenerator {
genericRequirements: genericRequirements,
)

case .range(let bound):
let boundJavaType = try translateGenericTypeParameter(bound, genericParameters: genericParameters, genericRequirements: genericRequirements)
let rangeType = JavaType.swiftRange(boundJavaType)
return TranslatedResult(
javaType: rangeType,
nativeJavaType: .class(package: "org.swift.swiftkit.core", name: "SwiftRange"),
outParameters: [],
conversion: .placeholder
)

case .set(let element):
return try translateSetResult(
elementType: element,
Expand Down Expand Up @@ -1159,6 +1176,9 @@ extension JNISwift2JavaGenerator {
)
return .swiftDictionaryMap(keyJavaType, valueJavaType)

case .range(let bound):
return .swiftRange(try translateGenericTypeParameter(bound, genericParameters: genericParameters, genericRequirements: genericRequirements))

case .set(let element):
let elementJavaType = try translateGenericTypeParameter(
element,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ extension JNISwift2JavaGenerator {
parameterName: parameterName
)

case .range(let bound):
let bridgeType = try bridgeTypeName(for: bound)
return NativeParameter(
parameters: [JavaParameter(name: parameterName, type: .class(package: "org.swift.swiftkit.core", name: "SwiftRange"))],
conversion: .constant("\(JNISwift2JavaGenerator.indirectVariableName(for: parameterName))"),
indirectConversion: .method(
.constant("SwiftRangeBridge<\(bridgeType)>"),
function: "fromJavaObject",
arguments: [(nil, .placeholder), ("in", .constant("environment"))]
),
conversionCheck: .validRange
)

case .set(let element):
return try translateSetParameter(
elementType: element,
Expand Down Expand Up @@ -350,6 +363,10 @@ extension JNISwift2JavaGenerator {
genericParameters: genericParameters,
genericRequirements: genericRequirements
)
// A tuple cannot currently propagate an element's validation step.
if case .validRange = elementNative.conversionCheck {
throw JavaTranslationError.unsupportedSwiftType(element.type)
}
allJNIParameters.append(contentsOf: elementNative.parameters)
elementConversions.append((label: element.label, conversion: elementNative.conversion))
}
Expand Down Expand Up @@ -812,6 +829,18 @@ extension JNISwift2JavaGenerator {
resultName: resultName
)

case .range(let bound):
let bridgeType = try bridgeTypeName(for: bound)
return NativeResult(
javaType: .class(package: "org.swift.swiftkit.core", name: "SwiftRange"),
conversion: .method(
.constant("SwiftRangeBridge<\(bridgeType)>"),
function: "toJavaObject",
arguments: [(nil, .placeholder), ("in", .constant("environment"))]
),
outParameters: []
)

case .set(let elementType):
return try translateSetResult(
elementType: elementType,
Expand Down Expand Up @@ -2155,10 +2184,13 @@ extension JNISwift2JavaGenerator {

enum NativeSwiftConversionCheck {
case check32BitIntOverflow(typeWithMinAndMax: SwiftType)
case validRange

// Returns the check string
func render(_ printer: inout SwiftPrinter, _ placeholder: String) -> String {
switch self {
case .validRange:
return "\(placeholder) != nil"
case .check32BitIntOverflow(let minMaxSource):
return "\(placeholder) >= \(minMaxSource).min && \(placeholder) <= \(minMaxSource).max"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,18 @@ extension JNISwift2JavaGenerator {
let lowered = parameter.conversion.render(&printer, javaParameterName)
arguments.append(lowered)

// Range conversion may post a Java exception. Stop before converting any
// subsequent arguments or calling Swift with invalid bounds.
if case .validRange = parameter.conversionCheck {
let name = JNISwift2JavaGenerator.indirectVariableName(for: javaParameterName)
let value = parameter.indirectConversion!.render(&printer, javaParameterName)
printer.print("let \(name) = \(value)")
printer.printGuardBlock("let \(name) = \(name)") { printer in
printer.print(dummyReturn(for: nativeSignature))
}
continue
}

parameter.indirectConversion.flatMap {
indirectVariables.append((javaParameterName, $0.render(&printer, javaParameterName)))
}
Expand All @@ -734,7 +746,7 @@ extension JNISwift2JavaGenerator {
JNISwift2JavaGenerator.indirectVariableName(for: javaParameterName),
)
)
case nil:
case .validRange, nil:
break
}
}
Expand Down
Loading
Loading