Skip to content

Commit c1b87ad

Browse files
Enabled import-side arrays. Updated Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift to allow array lowering/lifting (stack-based, no ABI params) and to handle stack-only parameters in CallJSEmission. Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift now lifts array parameters and lowers array returns for imported JS functions using existing array fragments. Sources/JavaScriptKit/BridgeJSIntrinsics.swift gives Array stack bridging conformance plus import helpers (bridgeJSLowerParameter, bridgeJSLiftReturn, bridgeJSLowerStackReturn), covering nested arrays. Added Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ImportArray.swift with corresponding snapshots for JSFunction array imports.
Tests: `UPDATE_SNAPSHOTS=1 swift test --package-path ./Plugins/BridgeJS --filter BridgeJSCodegenTests` followed by `swift test --package-path ./Plugins/BridgeJS --filter BridgeJSCodegenTests` (pass). Next steps: 1) Run the full BridgeJS test suite (`swift test --package-path ./Plugins/BridgeJS`) to ensure wider coverage. 2) Consider runtime-side coverage (e.g., `make unittest` with `JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1`) if you want to exercise the new import path end-to-end.
1 parent e55d927 commit c1b87ad

File tree

15 files changed

+1427
-63
lines changed

15 files changed

+1427
-63
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public struct ImportTS {
8383
var abiReturnType: WasmCoreType?
8484
// Track destructured variable names for multiple lowered parameters
8585
var destructuredVarNames: [String] = []
86+
// Stack-lowered parameters should be evaluated in reverse order to match LIFO stacks
87+
var stackLoweringStmts: [CodeBlockItemSyntax] = []
8688

8789
init(moduleName: String, abiName: String, context: BridgeContext = .importTS) {
8890
self.moduleName = moduleName
@@ -93,11 +95,6 @@ public struct ImportTS {
9395
func lowerParameter(param: Parameter) throws {
9496
let loweringInfo = try param.type.loweringParameterInfo(context: context)
9597

96-
// Generate destructured variable names for all lowered parameters
97-
let destructuredNames = loweringInfo.loweredParameters.map {
98-
"\(param.name)\($0.name.capitalizedFirstLetter)"
99-
}
100-
10198
let initializerExpr: ExprSyntax
10299
switch param.type {
103100
case .closure(let signature):
@@ -108,6 +105,34 @@ public struct ImportTS {
108105
initializerExpr = ExprSyntax("\(raw: param.name).bridgeJSLowerParameter()")
109106
}
110107

108+
if loweringInfo.loweredParameters.isEmpty {
109+
let stmt = CodeBlockItemSyntax(
110+
item: .decl(
111+
DeclSyntax(
112+
VariableDeclSyntax(
113+
bindingSpecifier: .keyword(.let),
114+
bindings: PatternBindingListSyntax {
115+
PatternBindingSyntax(
116+
pattern: PatternSyntax(
117+
IdentifierPatternSyntax(identifier: .wildcardToken())
118+
),
119+
initializer: InitializerClauseSyntax(value: initializerExpr)
120+
)
121+
}
122+
)
123+
)
124+
)
125+
)
126+
// Insert at the front so the last parameter ends up on the stack first
127+
stackLoweringStmts.insert(stmt, at: 0)
128+
return
129+
}
130+
131+
// Generate destructured variable names for all lowered parameters
132+
let destructuredNames = loweringInfo.loweredParameters.map {
133+
"\(param.name)\($0.name.capitalizedFirstLetter)"
134+
}
135+
111136
// Always add destructuring statement to body (unified for single and multiple)
112137
let pattern: PatternSyntax
113138
if destructuredNames.count == 1 {
@@ -166,6 +191,9 @@ public struct ImportTS {
166191
}
167192

168193
func call(returnType: BridgeType) throws {
194+
let liftingInfo = try returnType.liftingReturnInfo(context: context)
195+
// Stack-based lowerings must run in reverse parameter order (already reversed)
196+
body.append(contentsOf: stackLoweringStmts)
169197
// Build function call expression
170198
let callExpr = FunctionCallExprSyntax(
171199
calledExpression: ExprSyntax("\(raw: abiName)"),
@@ -183,6 +211,9 @@ public struct ImportTS {
183211
} else if returnType.usesSideChannelForOptionalReturn() {
184212
// Side channel returns don't need "let ret ="
185213
body.append(CodeBlockItemSyntax(item: .stmt(StmtSyntax(ExpressionStmtSyntax(expression: callExpr)))))
214+
} else if liftingInfo.valueToLift == nil {
215+
// Value is returned via stacks, no direct ABI return
216+
body.append(CodeBlockItemSyntax(item: .stmt(StmtSyntax(ExpressionStmtSyntax(expression: callExpr)))))
186217
} else {
187218
body.append("let ret = \(raw: callExpr)")
188219
}
@@ -922,13 +953,9 @@ extension BridgeType {
922953
return LoweringParameterInfo(loweredParameters: [("value", .i32)])
923954
}
924955
case .rawValueEnum(_, let rawType):
925-
switch context {
926-
case .importTS:
927-
return LoweringParameterInfo(loweredParameters: [("value", rawType.wasmCoreType ?? .i32)])
928-
case .exportSwift:
929-
// For protocol export we return .i32 for String raw value type instead of nil
930-
return LoweringParameterInfo(loweredParameters: [("value", rawType.wasmCoreType ?? .i32)])
931-
}
956+
// Both importTS and exportSwift lower raw-value enums the same way
957+
let wasmType = rawType.wasmCoreType ?? .i32
958+
return LoweringParameterInfo(loweredParameters: [("value", wasmType)])
932959
case .associatedValueEnum:
933960
switch context {
934961
case .importTS:
@@ -952,12 +979,7 @@ extension BridgeType {
952979
params.append(contentsOf: wrappedInfo.loweredParameters)
953980
return LoweringParameterInfo(loweredParameters: params)
954981
case .array:
955-
switch context {
956-
case .importTS:
957-
throw BridgeJSCoreError("Array types are not yet supported in TypeScript imports")
958-
case .exportSwift:
959-
return LoweringParameterInfo(loweredParameters: [])
960-
}
982+
return LoweringParameterInfo(loweredParameters: [])
961983
}
962984
}
963985

@@ -1011,13 +1033,9 @@ extension BridgeType {
10111033
return LiftingReturnInfo(valueToLift: .i32)
10121034
}
10131035
case .rawValueEnum(_, let rawType):
1014-
switch context {
1015-
case .importTS:
1016-
return LiftingReturnInfo(valueToLift: rawType.wasmCoreType ?? .i32)
1017-
case .exportSwift:
1018-
// For protocol export we return .i32 for String raw value type instead of nil
1019-
return LiftingReturnInfo(valueToLift: rawType.wasmCoreType ?? .i32)
1020-
}
1036+
// Both importTS and exportSwift lift raw-value enums identically
1037+
let wasmType = rawType.wasmCoreType ?? .i32
1038+
return LiftingReturnInfo(valueToLift: wasmType)
10211039
case .associatedValueEnum:
10221040
switch context {
10231041
case .importTS:
@@ -1039,12 +1057,7 @@ extension BridgeType {
10391057
let wrappedInfo = try wrappedType.liftingReturnInfo(context: context)
10401058
return LiftingReturnInfo(valueToLift: wrappedInfo.valueToLift)
10411059
case .array:
1042-
switch context {
1043-
case .importTS:
1044-
throw BridgeJSCoreError("Array types are not yet supported in TypeScript imports")
1045-
case .exportSwift:
1046-
return LiftingReturnInfo(valueToLift: nil)
1047-
}
1060+
return LiftingReturnInfo(valueToLift: nil)
10481061
}
10491062
}
10501063
}

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2147,12 +2147,10 @@ extension BridgeJSLink {
21472147

21482148
func liftParameter(param: Parameter) throws {
21492149
let liftingFragment = try IntrinsicJSFragment.liftParameter(type: param.type, context: context)
2150-
assert(
2151-
liftingFragment.parameters.count >= 1,
2152-
"Lifting fragment should have at least one parameter to lift"
2153-
)
21542150
let valuesToLift: [String]
2155-
if liftingFragment.parameters.count == 1 {
2151+
if liftingFragment.parameters.isEmpty {
2152+
valuesToLift = []
2153+
} else if liftingFragment.parameters.count == 1 {
21562154
parameterNames.append(param.name)
21572155
valuesToLift = [scope.variable(param.name)]
21582156
} else {

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,14 +1565,7 @@ struct IntrinsicJSFragment: Sendable {
15651565
"Namespace enums are not supported to be passed as parameters to imported JS functions: \(string)"
15661566
)
15671567
case .array(let elementType):
1568-
switch context {
1569-
case .importTS:
1570-
throw BridgeJSLinkError(
1571-
message: "Arrays are not yet supported to be passed as parameters to imported JS functions"
1572-
)
1573-
case .exportSwift:
1574-
return try arrayLift(elementType: elementType)
1575-
}
1568+
return try arrayLift(elementType: elementType)
15761569
}
15771570
}
15781571

@@ -1640,14 +1633,7 @@ struct IntrinsicJSFragment: Sendable {
16401633
message: "Namespace enums are not supported to be returned from imported JS functions: \(string)"
16411634
)
16421635
case .array(let elementType):
1643-
switch context {
1644-
case .importTS:
1645-
throw BridgeJSLinkError(
1646-
message: "Arrays are not yet supported to be returned from imported JS functions"
1647-
)
1648-
case .exportSwift:
1649-
return try arrayLower(elementType: elementType)
1650-
}
1636+
return try arrayLower(elementType: elementType)
16511637
}
16521638
}
16531639

@@ -2756,11 +2742,10 @@ struct IntrinsicJSFragment: Sendable {
27562742
// Lift return value if needed
27572743
if method.returnType != .void {
27582744
let liftFragment = try! IntrinsicJSFragment.liftReturn(type: method.returnType)
2759-
if !liftFragment.parameters.isEmpty {
2760-
let lifted = liftFragment.printCode(["ret"], methodScope, printer, methodCleanup)
2761-
if let liftedValue = lifted.first {
2762-
printer.write("return \(liftedValue);")
2763-
}
2745+
let liftArgs = liftFragment.parameters.isEmpty ? [] : ["ret"]
2746+
let lifted = liftFragment.printCode(liftArgs, methodScope, printer, methodCleanup)
2747+
if let liftedValue = lifted.first {
2748+
printer.write("return \(liftedValue);")
27642749
}
27652750
}
27662751
}
@@ -3090,7 +3075,7 @@ struct IntrinsicJSFragment: Sendable {
30903075
printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);")
30913076
cleanup.write("if (\(enumCleanupVar)) { \(enumCleanupVar)(); }")
30923077
default:
3093-
// For other types (nested structs, etc.), original logic applies
3078+
// For other types (including arrays), generate lowering under the isSome guard
30943079
let wrappedFragment = structFieldLowerFragment(
30953080
field: ExportedProperty(
30963081
name: field.name,
@@ -3100,9 +3085,37 @@ struct IntrinsicJSFragment: Sendable {
31003085
),
31013086
allStructs: allStructs
31023087
)
3088+
let guardedPrinter = CodeFragmentPrinter()
3089+
let guardedCleanup = CodeFragmentPrinter()
3090+
_ = wrappedFragment.printCode([value], scope, guardedPrinter, guardedCleanup)
3091+
var loweredLines = guardedPrinter.lines
3092+
var hoistedCleanupVar: String?
3093+
if let first = loweredLines.first {
3094+
let trimmed = first.trimmingCharacters(in: .whitespaces)
3095+
if trimmed.hasPrefix("const "),
3096+
let namePart = trimmed.split(separator: " ").dropFirst().first,
3097+
trimmed.contains("= []")
3098+
{
3099+
hoistedCleanupVar = String(namePart)
3100+
loweredLines[0] = "\(hoistedCleanupVar!) = [];"
3101+
}
3102+
}
3103+
if let hoistedName = hoistedCleanupVar {
3104+
printer.write("let \(hoistedName);")
3105+
}
31033106
printer.write("if (\(isSomeVar)) {")
31043107
printer.indent {
3105-
_ = wrappedFragment.printCode([value], scope, printer, cleanup)
3108+
for line in loweredLines {
3109+
printer.write(line)
3110+
}
3111+
// Cleanup is conditional on isSome as well
3112+
if !guardedCleanup.lines.isEmpty {
3113+
cleanup.write("if (\(isSomeVar)) {")
3114+
cleanup.indent {
3115+
cleanup.write(contentsOf: guardedCleanup)
3116+
}
3117+
cleanup.write("}")
3118+
}
31063119
}
31073120
printer.write("}")
31083121
printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@JSFunction func roundtrip(_ items: [Int]) throws(JSException) -> [Int]
2+
@JSFunction func logStrings(_ items: [String]) throws(JSException)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"exported" : {
3+
"classes" : [
4+
5+
],
6+
"enums" : [
7+
8+
],
9+
"exposeToGlobal" : false,
10+
"functions" : [
11+
12+
],
13+
"protocols" : [
14+
15+
],
16+
"structs" : [
17+
18+
]
19+
},
20+
"imported" : {
21+
"children" : [
22+
{
23+
"functions" : [
24+
{
25+
"name" : "roundtrip",
26+
"parameters" : [
27+
{
28+
"name" : "items",
29+
"type" : {
30+
"array" : {
31+
"_0" : {
32+
"int" : {
33+
34+
}
35+
}
36+
}
37+
}
38+
}
39+
],
40+
"returnType" : {
41+
"array" : {
42+
"_0" : {
43+
"int" : {
44+
45+
}
46+
}
47+
}
48+
}
49+
},
50+
{
51+
"name" : "logStrings",
52+
"parameters" : [
53+
{
54+
"name" : "items",
55+
"type" : {
56+
"array" : {
57+
"_0" : {
58+
"string" : {
59+
60+
}
61+
}
62+
}
63+
}
64+
}
65+
],
66+
"returnType" : {
67+
"void" : {
68+
69+
}
70+
}
71+
}
72+
],
73+
"types" : [
74+
75+
]
76+
}
77+
]
78+
},
79+
"moduleName" : "TestModule"
80+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#if arch(wasm32)
2+
@_extern(wasm, module: "TestModule", name: "bjs_roundtrip")
3+
fileprivate func bjs_roundtrip() -> Void
4+
#else
5+
fileprivate func bjs_roundtrip() -> Void {
6+
fatalError("Only available on WebAssembly")
7+
}
8+
#endif
9+
10+
func _$roundtrip(_ items: [Int]) throws(JSException) -> [Int] {
11+
let _ = items.bridgeJSLowerParameter()
12+
bjs_roundtrip()
13+
if let error = _swift_js_take_exception() {
14+
throw error
15+
}
16+
return [Int].bridgeJSLiftReturn()
17+
}
18+
19+
#if arch(wasm32)
20+
@_extern(wasm, module: "TestModule", name: "bjs_logStrings")
21+
fileprivate func bjs_logStrings() -> Void
22+
#else
23+
fileprivate func bjs_logStrings() -> Void {
24+
fatalError("Only available on WebAssembly")
25+
}
26+
#endif
27+
28+
func _$logStrings(_ items: [String]) throws(JSException) -> Void {
29+
let _ = items.bridgeJSLowerParameter()
30+
bjs_logStrings()
31+
if let error = _swift_js_take_exception() {
32+
throw error
33+
}
34+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
2+
// DO NOT EDIT.
3+
//
4+
// To update this file, just rebuild your project or run
5+
// `swift package bridge-js`.
6+
7+
export type Exports = {
8+
}
9+
export type Imports = {
10+
roundtrip(items: number[]): number[];
11+
logStrings(items: string[]): void;
12+
}
13+
export function createInstantiator(options: {
14+
imports: Imports;
15+
}, swift: any): Promise<{
16+
addImports: (importObject: WebAssembly.Imports) => void;
17+
setInstance: (instance: WebAssembly.Instance) => void;
18+
createExports: (instance: WebAssembly.Instance) => Exports;
19+
}>;

0 commit comments

Comments
 (0)