From 9610bebd60ce572d4244d23e58a8f343774f1d9f Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 23 Jun 2026 12:38:11 +0900 Subject: [PATCH] SwiftExtract: fix subscript label handling A subscript declaration `subscript(index: Int)` is called as `obj[5]`, not `obj[index: 5]`. --- ...Swift2JavaGenerator+FunctionLowering.swift | 12 ++- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 21 ++++- .../SwiftTypes/SwiftFunctionSignature.swift | 15 +++- .../JNI/JNISubscriptsTests.swift | 79 ++++++++++++++++++- 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 5b577f52b..4fce2e430 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -1046,7 +1046,11 @@ extension LoweredFunctionSignature { fatalError("Enum cases are not supported with FFM.") case .subscriptGetter: - let parameters = paramExprs.map { $0.description }.joined(separator: ", ") + let parameters = paramExprs.enumerated() + .map { (i, argument) -> String in + LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argument).description + } + .joined(separator: ", ") resultExpr = "\(callee)[\(raw: parameters)]" case .subscriptSetter: assert(paramExprs.count >= 1) @@ -1054,7 +1058,11 @@ extension LoweredFunctionSignature { var argumentsWithoutNewValue = paramExprs let newValueArgument = argumentsWithoutNewValue.removeLast() - let parameters = argumentsWithoutNewValue.map { $0.description }.joined(separator: ", ") + let parameters = argumentsWithoutNewValue.enumerated() + .map { (i, argument) -> String in + LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argument).description + } + .joined(separator: ", ") resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)" } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index bf0b7f603..5d976619c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -660,17 +660,30 @@ extension JNISwift2JavaGenerator { result = "\(callee).\(decl.name) = \(newValueArgument)" case .subscriptGetter: - let parameters = arguments.joined(separator: ", ") + let parameters = zip( + decl.functionSignature.parameters, + arguments, + ).map { originalParam, argument in + let label = originalParam.argumentLabel.map { "\($0): " } ?? "" + return "\(label)\(argument)" + } + .joined(separator: ", ") result = "\(callee)[\(parameters)]" case .subscriptSetter: guard let newValueArgument = arguments.last else { fatalError("Setter did not contain newValue parameter: \(decl)") } - var argumentsWithoutNewValue = arguments - argumentsWithoutNewValue.removeLast() + // Drop the trailing newValue parameter — it's the right-hand-side of + // the assignment, not part of the bracketed index expression + let indexParams = decl.functionSignature.parameters.dropLast() + let indexArgs = arguments.dropLast() - let parameters = argumentsWithoutNewValue.joined(separator: ", ") + let parameters = zip(indexParams, indexArgs).map { originalParam, argument in + let label = originalParam.argumentLabel.map { "\($0): " } ?? "" + return "\(label)\(argument)" + } + .joined(separator: ", ") result = "\(callee)[\(parameters)] = \(newValueArgument)" } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 477fb5f2e..15a147a08 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -358,8 +358,19 @@ extension SwiftFunctionSignature { ) let valueType: SwiftType = try SwiftType(subscriptNode.returnClause.type, lookupContext: lookupContext) - var nodeParameters = try subscriptNode.parameterClause.parameters.map { param in - try SwiftParameter(param, lookupContext: lookupContext) + var nodeParameters = try subscriptNode.parameterClause.parameters.map { param -> SwiftParameter in + var p = try SwiftParameter(param, lookupContext: lookupContext) + // Subscript parameters have no external argument label by default. A + // single-identifier subscript parameter `subscript(index: Int)` is + // called as `obj[5]`, not `obj[index: 5]` — only the explicit + // `subscript(label name: Int)` form (i.e. a `secondName` exists) + // surfaces an external label. SwiftParameter.init treats single- + // identifier params like function params (label == name); strip the + // label here for the subscript-specific call shape. + if param.secondName == nil { + p.argumentLabel = nil + } + return p } var effectSpecifiers: [SwiftEffectSpecifier]? = nil diff --git a/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift index 8ad075649..4f241e517 100644 --- a/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift @@ -39,6 +39,17 @@ struct JNISubscriptsTests { } """ + private let subscriptWithLabelledParamsSource = """ + public struct MyStruct { + private var testVariable: [Int32] = [] + + public subscript(index idx: Int32) -> Int32 { + get { return testVariable[Int(idx)] } + set { testVariable[Int(idx)] = newValue } + } + } + """ + @Test("Test generation of JavaClass for subscript with no parameters") func generatesJavaClassForNoParams() throws { try assertOutput( @@ -84,7 +95,73 @@ struct JNISubscriptsTests { MyStruct.$setSubscript(index, newValue, this.$memoryAddress()); """, """ - private static native void $setSubscript(int index, int newValue, long selfPointer); + private static native void $setSubscript(int index, int newValue, long selfPointer); + """, + ] + ) + + // The subscipt uses no label, just directly: `base[idx]` + try assertOutput( + input: subscriptWithParamsSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__IJ") + ... + return selfPointer$.pointee[Int32(fromJNI: index, in: environment)].getJNILocalRefValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__IIJ") + ... + selfPointer$.pointee[Int32(fromJNI: index, in: environment)] = Int32(fromJNI: newValue, in: environment) + """, + ] + ) + } + + @Test("Test generation of JavaClass for subscript with labelled parameters") + func generatesJavaClassForLabelledParameters() throws { + try assertOutput( + input: subscriptWithLabelledParamsSource, + .jni, + .java, + expectedChunks: [ + """ + public int getSubscript(int idx) { + return MyStruct.$getSubscript(idx, this.$memoryAddress()); + + """, + """ + private static native int $getSubscript(int idx, long selfPointer); + """, + """ + public void setSubscript(int idx, int newValue) { + MyStruct.$setSubscript(idx, newValue, this.$memoryAddress()); + """, + """ + private static native void $setSubscript(int idx, int newValue, long selfPointer); + """, + ] + ) + + // The subscipt uses an explicit label: `base[index: idx]` + try assertOutput( + input: subscriptWithLabelledParamsSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__IJ") + ... + return selfPointer$.pointee[index: Int32(fromJNI: idx, in: environment)].getJNILocalRefValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__IIJ") + ... + selfPointer$.pointee[index: Int32(fromJNI: idx, in: environment)] = Int32(fromJNI: newValue, in: environment) """, ] )