Skip to content

Commit cffb411

Browse files
authored
jextract: Initial typealias handling (#714)
1 parent 1658b2d commit cffb411

11 files changed

Lines changed: 754 additions & 5 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 typealias Amount = Double
16+
17+
public struct TypealiasUser {
18+
public var amount: Amount
19+
20+
public init(amount: Amount) {
21+
self.amount = amount
22+
}
23+
24+
public func doubled() -> Amount {
25+
amount * 2
26+
}
27+
}
28+
29+
public func makeAmount(_ value: Amount) -> Amount {
30+
value
31+
}
32+
33+
// Generic typealias used with a use-site argument. The alias's generic
34+
// parameter `T` is substituted at the use site so `Maybe<Int64>` resolves
35+
// to `Optional<Int64>`, which is `java.lang.OptionalLong` in Java.
36+
public typealias Maybe<T> = T?
37+
38+
public func unwrapOrZero(_ value: Maybe<Int64>) -> Int64 {
39+
value ?? 0
40+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.ffm.AllocatingSwiftArena;
19+
20+
import java.util.OptionalLong;
21+
22+
import static org.junit.jupiter.api.Assertions.*;
23+
24+
public class TypealiasUserTest {
25+
@Test
26+
void plainTypealiasResolvesStructMembers() {
27+
try (var arena = AllocatingSwiftArena.ofConfined()) {
28+
var user = TypealiasUser.init(2.5, arena);
29+
assertEquals(2.5, user.getAmount(), 0.0);
30+
assertEquals(5.0, user.doubled(), 0.0);
31+
32+
user.setAmount(7.0);
33+
assertEquals(7.0, user.getAmount(), 0.0);
34+
}
35+
}
36+
37+
@Test
38+
void freeFunctionThroughAliasIsExported() {
39+
assertEquals(42.0, MySwiftLibrary.makeAmount(42.0), 0.0);
40+
}
41+
42+
@Test
43+
void genericTypeAliasSubstitutesUseSiteArguments() {
44+
// `Maybe<Int64>` substitutes T -> Int64, resolving to Optional<Int64>,
45+
// which is then mapped to java.util.OptionalLong.
46+
assertEquals(0L, MySwiftLibrary.unwrapOrZero(OptionalLong.empty()));
47+
assertEquals(123L, MySwiftLibrary.unwrapOrZero(OptionalLong.of(123L)));
48+
}
49+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 typealias Amount = Double
16+
17+
public struct TypealiasUser {
18+
public var amount: Amount
19+
20+
public init(amount: Amount) {
21+
self.amount = amount
22+
}
23+
24+
public func doubled() -> Amount {
25+
amount * 2
26+
}
27+
}
28+
29+
public func makeAmount(_ value: Amount) -> Amount {
30+
value
31+
}
32+
33+
// Generic typealias used with a use-site argument. The alias's generic
34+
// parameter `T` is substituted at the use site so `Maybe<Int64>` resolves
35+
// to `Optional<Int64>`, which is `java.lang.OptionalLong` in Java.
36+
public typealias Maybe<T> = T?
37+
38+
public func unwrapOrZero(_ value: Maybe<Int64>) -> Int64 {
39+
value ?? 0
40+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.SwiftArena;
19+
20+
import java.util.OptionalLong;
21+
22+
import static org.junit.jupiter.api.Assertions.*;
23+
24+
public class TypealiasUserTest {
25+
@Test
26+
void plainTypealiasResolvesStructMembers() {
27+
try (var arena = SwiftArena.ofConfined()) {
28+
var user = TypealiasUser.init(2.5, arena);
29+
assertEquals(2.5, user.getAmount(), 0.0);
30+
assertEquals(5.0, user.doubled(), 0.0);
31+
32+
user.setAmount(7.0);
33+
assertEquals(7.0, user.getAmount(), 0.0);
34+
}
35+
}
36+
37+
@Test
38+
void freeFunctionThroughAliasIsExported() {
39+
assertEquals(42.0, MySwiftLibrary.makeAmount(42.0), 0.0);
40+
}
41+
42+
@Test
43+
void genericTypeAliasSubstitutesUseSiteArguments() {
44+
// `Maybe<Int64>` substitutes T -> Int64, resolving to Optional<Int64>,
45+
// which is then mapped to java.util.OptionalLong.
46+
assertEquals(0L, MySwiftLibrary.unwrapOrZero(OptionalLong.empty()));
47+
assertEquals(123L, MySwiftLibrary.unwrapOrZero(OptionalLong.of(123L)));
48+
}
49+
}

Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol {
2828
/// The top-level nominal types, found by name.
2929
var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:]
3030

31+
/// The top-level typealias declarations, found by name.
32+
var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:]
33+
3134
/// The nested types defined within this module. The map itself is indexed by the
3235
/// identifier of the nominal type declaration, and each entry is a map from the nested
3336
/// type name to the nominal type declaration.
@@ -38,6 +41,11 @@ struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol {
3841
topLevelTypes[name]
3942
}
4043

44+
/// Look for a top-level typealias with the given name.
45+
func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? {
46+
topLevelTypeAliases[name]
47+
}
48+
4149
// Look for a nested type with the given name.
4250
func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? {
4351
nestedTypes[parent]?[name]

Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,24 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration {
203203
}
204204
}
205205

206+
/// A plain typealias will resolve as the right hand type in generated code.
207+
///
208+
/// A typealias used as a specialization of a generic type will be emitted as
209+
/// a new concrete type in the Java. This way we can specialize `FishBox` from
210+
/// `Box<T>` by doing `typealias FishBox = Box<Fish>`.
211+
package final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration {
212+
let syntax: TypeAliasDeclSyntax
213+
214+
init(
215+
sourceFilePath: String,
216+
moduleName: String,
217+
node: TypeAliasDeclSyntax
218+
) {
219+
self.syntax = node
220+
super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text)
221+
}
222+
}
223+
206224
extension SwiftTypeDeclaration: Equatable {
207225
package static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool {
208226
lhs === rhs

Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,29 @@ extension SwiftParsedModuleSymbolTableBuilder {
8181
self.handle(extensionDecl: extensionNode, sourceFilePath: sourceFilePath)
8282
} else if let ifConfigNode = decl.as(IfConfigDeclSyntax.self) {
8383
self.handle(ifConfig: ifConfigNode, sourceFilePath: sourceFilePath)
84+
} else if let typeAliasNode = decl.as(TypeAliasDeclSyntax.self) {
85+
self.handle(typeAliasDecl: typeAliasNode, sourceFilePath: sourceFilePath)
8486
}
8587
}
8688

89+
mutating func handle(
90+
typeAliasDecl node: TypeAliasDeclSyntax,
91+
sourceFilePath: String
92+
) {
93+
let name = node.name.text
94+
if symbolTable.topLevelTypeAliases[name] != nil
95+
|| symbolTable.lookupTopLevelNominalType(name) != nil
96+
{
97+
log?.debug("Failed to add a typealias into symbol table: redeclaration; \(name)")
98+
return
99+
}
100+
symbolTable.topLevelTypeAliases[name] = SwiftTypeAliasDeclaration(
101+
sourceFilePath: sourceFilePath,
102+
moduleName: moduleName,
103+
node: node
104+
)
105+
}
106+
87107
/// Add a nominal type declaration and all of the nested types within it to the symbol
88108
/// table.
89109
mutating func handle(

Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ package protocol SwiftSymbolTableProtocol {
2828

2929
// Look for a nested type with the given name.
3030
func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration?
31+
32+
/// Look for a top-level typealias with the given name.
33+
func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration?
3134
}
3235

3336
extension SwiftSymbolTableProtocol {
@@ -178,6 +181,21 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol {
178181

179182
return nil
180183
}
184+
185+
/// Look for a top-level typealias with the given name.
186+
package func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? {
187+
if let parsedResult = parsedModule.lookupTopLevelTypealias(name) {
188+
return parsedResult
189+
}
190+
191+
for importedModule in prioritySortedImportedModules {
192+
if let result = importedModule.lookupTopLevelTypealias(name) {
193+
return result
194+
}
195+
}
196+
197+
return nil
198+
}
181199
}
182200

183201
extension SwiftSymbolTable {

Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,29 @@ extension SwiftType {
466466
)
467467
} else if let genericParamDecl = typeDecl as? SwiftGenericParameterDeclaration {
468468
self = .genericParameter(genericParamDecl)
469+
} else if let aliasDecl = typeDecl as? SwiftTypeAliasDeclaration {
470+
let aliasGenericParams =
471+
aliasDecl.syntax.genericParameterClause?.parameters.map { $0.name.text } ?? []
472+
let useSiteArgs = genericArguments ?? []
473+
474+
// The alias's generic parameter count must match the use-site argument
475+
// count. Treat any mismatch (including use-site args on a non-generic
476+
// alias, or missing args on a generic alias) as unimplemented to fall
477+
// through to silent drop.
478+
guard aliasGenericParams.count == useSiteArgs.count else {
479+
throw TypeTranslationError.unimplementedType(originalType)
480+
}
481+
482+
let resolved = try lookupContext.resolve(typeAlias: aliasDecl)
483+
484+
if aliasGenericParams.isEmpty {
485+
self = resolved
486+
} else {
487+
let substitutions = Dictionary(
488+
uniqueKeysWithValues: zip(aliasGenericParams, useSiteArgs)
489+
)
490+
self = resolved.substituting(genericParameters: substitutions)
491+
}
469492
} else {
470493
fatalError("unknown SwiftTypeDeclaration: \(type(of: typeDecl))")
471494
}
@@ -494,6 +517,55 @@ extension SwiftType {
494517
)
495518
}
496519

520+
/// Substitute generic parameters *by name*.
521+
///
522+
/// This is used e.g. by typealiases like `typealias Ano<T> = Array<T>`,
523+
/// so usages like `Ano<Int>` become `Array<Int>`.
524+
func substituting(genericParameters substitutions: [String: SwiftType]) -> SwiftType {
525+
guard !substitutions.isEmpty else { return self }
526+
527+
switch self {
528+
case .nominal(let nominal):
529+
return .nominal(
530+
SwiftNominalType(
531+
parent: nominal.parent,
532+
sugarName: nominal.sugarName,
533+
nominalTypeDecl: nominal.nominalTypeDecl,
534+
genericArguments: nominal.genericArguments?.map {
535+
$0.substituting(genericParameters: substitutions)
536+
}
537+
)
538+
)
539+
case .genericParameter(let decl):
540+
return substitutions[decl.name] ?? self
541+
case .function(var fn):
542+
fn.parameters = fn.parameters.map { p in
543+
var p = p
544+
p.type = p.type.substituting(genericParameters: substitutions)
545+
return p
546+
}
547+
fn.resultType = fn.resultType.substituting(genericParameters: substitutions)
548+
return .function(fn)
549+
case .metatype(let inner):
550+
return .metatype(inner.substituting(genericParameters: substitutions))
551+
case .tuple(let elements):
552+
return .tuple(
553+
elements.map {
554+
SwiftTupleElement(
555+
label: $0.label,
556+
type: $0.type.substituting(genericParameters: substitutions)
557+
)
558+
}
559+
)
560+
case .existential(let inner):
561+
return .existential(inner.substituting(genericParameters: substitutions))
562+
case .opaque(let inner):
563+
return .opaque(inner.substituting(genericParameters: substitutions))
564+
case .composite(let types):
565+
return .composite(types.map { $0.substituting(genericParameters: substitutions) })
566+
}
567+
}
568+
497569
/// Produce an expression that creates the metatype for this type in
498570
/// Swift source code.
499571
var metatypeReferenceExprSyntax: ExprSyntax {

0 commit comments

Comments
 (0)