Skip to content

Commit e7b4cde

Browse files
committed
SwiftDictionaryMap - a native backed Map implementation
resolves #609
1 parent 0c7b613 commit e7b4cde

38 files changed

Lines changed: 1418 additions & 69 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public func makeStringToLongDictionary() -> [String: Int64] {
16+
[
17+
"hello": 1,
18+
"world": 2,
19+
]
20+
}
21+
22+
public func stringToLongDictionary(dict: [String: Int64]) -> [String: Int64] {
23+
dict
24+
}
25+
26+
public func insertIntoStringToLongDictionary(dict: [String: Int64], key: String, value: Int64) -> [String: Int64] {
27+
var copy = dict
28+
copy[key] = value
29+
return copy
30+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package com.example.swift;
16+
17+
import java.util.HashMap;
18+
import java.util.Map;
19+
import org.junit.jupiter.api.Test;
20+
import org.swift.swiftkit.core.collections.SwiftDictionaryMap;
21+
import org.swift.swiftkit.core.SwiftArena;
22+
23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
public class SwiftDictionaryMapTest {
26+
@Test
27+
void makeStringToLongDictionary() {
28+
try (var arena = SwiftArena.ofConfined()) {
29+
SwiftDictionaryMap<String, Long> dict = MySwiftLibrary.makeStringToLongDictionary(arena);
30+
assertEquals(2, dict.size());
31+
assertEquals(1L, dict.get("hello"));
32+
assertEquals(2L, dict.get("world"));
33+
assertTrue(dict.containsKey("hello"));
34+
assertFalse(dict.containsKey("missing"));
35+
assertNull(dict.get("missing"));
36+
}
37+
}
38+
39+
@Test
40+
void stringToLongDictionaryRoundtrip() {
41+
try (var arena = SwiftArena.ofConfined()) {
42+
SwiftDictionaryMap<String, Long> original = MySwiftLibrary.makeStringToLongDictionary(arena);
43+
SwiftDictionaryMap<String, Long> roundtripped = MySwiftLibrary.stringToLongDictionary(original, arena);
44+
assertEquals(original.size(), roundtripped.size());
45+
assertEquals(original.get("hello"), roundtripped.get("hello"));
46+
assertEquals(original.get("world"), roundtripped.get("world"));
47+
}
48+
}
49+
50+
@Test
51+
void insertIntoStringToLongDictionary() {
52+
try (var arena = SwiftArena.ofConfined()) {
53+
SwiftDictionaryMap<String, Long> original = MySwiftLibrary.makeStringToLongDictionary(arena);
54+
assertEquals(2, original.size());
55+
56+
// Insert a new key by passing the dictionary through Swift
57+
SwiftDictionaryMap<String, Long> modified =
58+
MySwiftLibrary.insertIntoStringToLongDictionary(original, "swift", 42L, arena);
59+
60+
// The modified dictionary has the new key
61+
assertEquals(3, modified.size());
62+
assertEquals(1L, modified.get("hello"));
63+
assertEquals(2L, modified.get("world"));
64+
assertEquals(42L, modified.get("swift"));
65+
66+
// The original dictionary is unchanged (Swift value semantics — it's a copy)
67+
assertEquals(2, original.size());
68+
assertNull(original.get("swift"));
69+
}
70+
}
71+
72+
@Test
73+
void toJavaMap() {
74+
Map<String, Long> javaMap;
75+
try (var arena = SwiftArena.ofConfined()) {
76+
SwiftDictionaryMap<String, Long> dict = MySwiftLibrary.makeStringToLongDictionary(arena);
77+
javaMap = dict.toJavaMap();
78+
79+
// The copy has the same contents as the original
80+
assertEquals(2, javaMap.size());
81+
assertEquals(1L, javaMap.get("hello"));
82+
assertEquals(2L, javaMap.get("world"));
83+
assertTrue(javaMap.containsKey("hello"));
84+
assertFalse(javaMap.containsKey("missing"));
85+
86+
// It's a plain HashMap, not the native-backed map
87+
assertInstanceOf(HashMap.class, javaMap);
88+
}
89+
90+
// The Java map copy survives arena closure
91+
assertEquals(2, javaMap.size());
92+
assertEquals(1L, javaMap.get("hello"));
93+
assertEquals(2L, javaMap.get("world"));
94+
}
95+
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension CType {
7070
case .optional(let wrapped) where wrapped.isPointer:
7171
try self.init(cdeclType: wrapped)
7272

73-
case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array:
73+
case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array, .dictionary:
7474
throw CDeclToCLoweringError.invalidCDeclType(cdeclType)
7575
}
7676
}
@@ -130,11 +130,8 @@ extension SwiftKnownTypeDeclKind {
130130
case .array:
131131
.pointer(.qualified(const: false, volatile: false, type: .void))
132132
case .void: .void
133-
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
134-
.unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .foundationData, .foundationDataProtocol,
135-
.essentialsData, .essentialsDataProtocol, .optional, .foundationDate, .essentialsDate, .foundationUUID,
136-
.essentialsUUID:
137-
nil
133+
default:
134+
nil // Since we know the set of all primitives, we can safely assume all others are not primitive
138135
}
139136
}
140137
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@ struct CdeclLowering {
435435

436436
case .array:
437437
throw LoweringError.unhandledType(type)
438+
439+
case .dictionary:
440+
throw LoweringError.unhandledType(type)
438441
}
439442
}
440443

@@ -527,7 +530,7 @@ struct CdeclLowering {
527530
}
528531
throw LoweringError.unhandledType(.optional(wrappedType))
529532

530-
case .function, .metatype, .optional, .composite, .array:
533+
case .function, .metatype, .optional, .composite, .array, .dictionary:
531534
throw LoweringError.unhandledType(.optional(wrappedType))
532535
}
533536
}
@@ -629,7 +632,7 @@ struct CdeclLowering {
629632
// Custom types are not supported yet.
630633
throw LoweringError.unhandledType(type)
631634

632-
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array:
635+
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array, .dictionary:
633636
// TODO: Implement
634637
throw LoweringError.unhandledType(type)
635638
}
@@ -832,7 +835,7 @@ struct CdeclLowering {
832835
)
833836
)
834837

835-
case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array:
838+
case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array, .dictionary:
836839
throw LoweringError.unhandledType(type)
837840
}
838841
}

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,9 @@ extension FFMSwift2JavaGenerator {
508508

509509
case .array:
510510
throw JavaTranslationError.unhandledType(swiftType)
511+
512+
case .dictionary:
513+
throw JavaTranslationError.unhandledType(swiftType)
511514
}
512515
}
513516

@@ -739,7 +742,7 @@ extension FFMSwift2JavaGenerator {
739742
)
740743
)
741744

742-
case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array:
745+
case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array, .dictionary:
743746
throw JavaTranslationError.unhandledType(swiftType)
744747
}
745748

Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ enum JNIJavaTypeTranslator {
4949
.foundationData, .foundationDataProtocol,
5050
.essentialsData, .essentialsDataProtocol,
5151
.array,
52+
.dictionary,
5253
.foundationDate, .essentialsDate,
5354
.foundationUUID, .essentialsUUID:
5455
return nil
@@ -73,6 +74,7 @@ enum JNIJavaTypeTranslator {
7374
.foundationData, .foundationDataProtocol,
7475
.essentialsData, .essentialsDataProtocol,
7576
.array,
77+
.dictionary,
7678
.foundationDate, .essentialsDate,
7779
.foundationUUID, .essentialsUUID:
7880
nil
@@ -97,6 +99,7 @@ enum JNIJavaTypeTranslator {
9799
.foundationData, .foundationDataProtocol,
98100
.essentialsData, .essentialsDataProtocol,
99101
.array,
102+
.dictionary,
100103
.foundationDate, .essentialsDate,
101104
.foundationUUID, .essentialsUUID:
102105
nil

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ extension JNISwift2JavaGenerator {
238238
case .array(let elementType):
239239
return try translateArrayParameter(name: parameterName, elementType: elementType)
240240

241+
case .dictionary:
242+
throw JavaTranslationError.unsupportedSwiftType(type)
243+
241244
case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite:
242245
throw JavaTranslationError.unsupportedSwiftType(type)
243246
}
@@ -256,7 +259,7 @@ extension JNISwift2JavaGenerator {
256259
)
257260
)
258261

259-
case .array, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
262+
case .array, .dictionary, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
260263
throw JavaTranslationError.unsupportedSwiftType(.array(elementType))
261264
}
262265
}
@@ -324,6 +327,9 @@ extension JNISwift2JavaGenerator {
324327
case .array(let elementType):
325328
return try self.translateArrayResult(elementType: elementType)
326329

330+
case .dictionary:
331+
throw JavaTranslationError.unsupportedSwiftType(type)
332+
327333
case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite:
328334
throw JavaTranslationError.unsupportedSwiftType(type)
329335
}
@@ -342,7 +348,7 @@ extension JNISwift2JavaGenerator {
342348
)
343349
)
344350

345-
case .array, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
351+
case .array, .dictionary, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
346352
throw JavaTranslationError.unsupportedSwiftType(.array(elementType))
347353
}
348354
}
@@ -478,7 +484,7 @@ extension SwiftType {
478484
case .array(let elementType):
479485
return elementType.isDirectlyTranslatedToWrapJava
480486

481-
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite:
487+
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .dictionary:
482488
return false
483489
}
484490
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ extension JNISwift2JavaGenerator {
2323
static let defaultJavaImports: [String] = [
2424
"org.swift.swiftkit.core.*",
2525
"org.swift.swiftkit.core.util.*",
26+
"org.swift.swiftkit.core.collections.*",
2627
"java.util.*",
2728
"java.util.concurrent.atomic.AtomicBoolean",
2829

0 commit comments

Comments
 (0)