Skip to content

Commit effae5d

Browse files
committed
BridgeJS: Add OptionalConvention enum and compositional optional lower/lift parameter handling
1 parent 7ea4e16 commit effae5d

File tree

16 files changed

+853
-567
lines changed

16 files changed

+853
-567
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,7 @@ struct StackCodegen {
725725
return "\(raw: type.swiftType).bridgeJSLiftParameter()"
726726
case .jsObject(let className?):
727727
return "\(raw: className)(unsafelyWrapping: JSObject.bridgeJSLiftParameter())"
728+
728729
case .nullable(let wrappedType, let kind):
729730
return liftNullableExpression(wrappedType: wrappedType, kind: kind)
730731
case .array(let elementType):
@@ -948,7 +949,7 @@ struct StackCodegen {
948949
accessor: String,
949950
varPrefix: String
950951
) -> [CodeBlockItemSyntax] {
951-
if wrappedType.descriptor.optionalUsesStackABI {
952+
if wrappedType.descriptor.optionalConvention == .stackABI {
952953
return ["\(raw: accessor).bridgeJSLowerReturn()"]
953954
}
954955

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 138 additions & 199 deletions
Large diffs are not rendered by default.

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -177,37 +177,64 @@ public enum WasmCoreType: String, Codable, Sendable {
177177

178178
// MARK: - ABI Descriptor
179179

180+
/// How `Optional<T>` is represented at the WASM ABI boundary.
181+
///
182+
/// The convention is derived from T's descriptor and determines how codegen
183+
/// handles nullable values in both the parameter and return directions.
184+
/// Stack ABI is the general-purpose fallback that works for any T.
185+
public enum OptionalConvention: Sendable, Equatable {
186+
/// Everything goes through the stack (isSome flag + payload pushed/popped).
187+
/// Used for types whose base representation already uses the stack (struct, array, dictionary).
188+
/// This is also the default fallback for unknown types.
189+
case stackABI
190+
191+
/// isSome is passed as an inline WASM parameter alongside T's normal parameters.
192+
/// For returns, T's return type carries the value (no side channel needed).
193+
/// Used for types with compact representations where the value space has no sentinel
194+
/// (bool, jsValue, closure, caseEnum, associatedValueEnum).
195+
case inlineFlag
196+
197+
/// Return value goes through a side-channel storage variable; WASM function returns void.
198+
/// For parameters, behaves like `.inlineFlag` (isSome + T's params as direct WASM params).
199+
/// Used for scalar types where Optional return needs disambiguation (int, string, jsObject, etc.).
200+
case sideChannelReturn
201+
}
202+
180203
/// Captures the WASM ABI shape for a ``BridgeType`` so codegen can read descriptor fields
181204
/// instead of switching on every concrete type.
182205
///
183-
/// `wasmReturnType` is for the export direction (SwiftJS), `importReturnType` for import
184-
/// (JSSwift). They differ for types like `string` and `associatedValueEnum` where the
206+
/// `wasmReturnType` is for the export direction (Swift->JS), `importReturnType` for import
207+
/// (JS->Swift). They differ for types like `string` and `associatedValueEnum` where the
185208
/// import side returns a pointer/ID while the export side uses the stack.
186209
public struct BridgeTypeDescriptor: Sendable {
187210
public let wasmParams: [(name: String, type: WasmCoreType)]
188211
public let wasmReturnType: WasmCoreType?
189212
public let importReturnType: WasmCoreType?
190-
public let optionalUsesStackABI: Bool
191-
public let optionalUsesSideChannelReturn: Bool
213+
public let optionalConvention: OptionalConvention
192214
public let usesStackLifting: Bool
193215
public let accessorTransform: AccessorTransform
194216
public let lowerMethod: LowerMethod
195217

218+
/// Creates a descriptor with an explicitly specified optional convention.
219+
///
220+
/// When `optionalConvention` is nil, it is derived from `wasmParams`:
221+
/// - Empty `wasmParams` (stack-based types) default to `.stackABI`
222+
/// - Non-empty `wasmParams` (scalar types) default to `.inlineFlag`
223+
///
224+
/// Only `.sideChannelReturn` needs to be explicitly specified.
196225
public init(
197226
wasmParams: [(name: String, type: WasmCoreType)],
198227
wasmReturnType: WasmCoreType?,
199228
importReturnType: WasmCoreType?? = nil,
200-
optionalUsesStackABI: Bool,
201-
optionalUsesSideChannelReturn: Bool = false,
229+
optionalConvention: OptionalConvention? = nil,
202230
usesStackLifting: Bool = false,
203231
accessorTransform: AccessorTransform,
204232
lowerMethod: LowerMethod
205233
) {
206234
self.wasmParams = wasmParams
207235
self.wasmReturnType = wasmReturnType
208236
self.importReturnType = importReturnType ?? wasmReturnType
209-
self.optionalUsesStackABI = optionalUsesStackABI
210-
self.optionalUsesSideChannelReturn = optionalUsesSideChannelReturn
237+
self.optionalConvention = optionalConvention ?? (wasmParams.isEmpty ? .stackABI : .inlineFlag)
211238
self.usesStackLifting = usesStackLifting
212239
self.accessorTransform = accessorTransform
213240
self.lowerMethod = lowerMethod
@@ -291,43 +318,38 @@ extension BridgeType {
291318
return BridgeTypeDescriptor(
292319
wasmParams: [("value", .i32)],
293320
wasmReturnType: .i32,
294-
optionalUsesStackABI: false,
295321
accessorTransform: .identity,
296322
lowerMethod: .stackReturn
297323
)
298324
case .int:
299325
return BridgeTypeDescriptor(
300326
wasmParams: [("value", .i32)],
301327
wasmReturnType: .i32,
302-
optionalUsesStackABI: false,
303-
optionalUsesSideChannelReturn: true,
328+
optionalConvention: .sideChannelReturn,
304329
accessorTransform: .identity,
305330
lowerMethod: .stackReturn
306331
)
307332
case .uint:
308333
return BridgeTypeDescriptor(
309334
wasmParams: [("value", .i32)],
310335
wasmReturnType: .i32,
311-
optionalUsesStackABI: false,
312-
optionalUsesSideChannelReturn: true,
336+
optionalConvention: .sideChannelReturn,
313337
accessorTransform: .identity,
314338
lowerMethod: .stackReturn
315339
)
316340
case .float:
317341
return BridgeTypeDescriptor(
318342
wasmParams: [("value", .f32)],
319343
wasmReturnType: .f32,
320-
optionalUsesStackABI: false,
321-
optionalUsesSideChannelReturn: true,
344+
optionalConvention: .sideChannelReturn,
322345
accessorTransform: .identity,
323346
lowerMethod: .stackReturn
324347
)
325348
case .double:
326349
return BridgeTypeDescriptor(
327350
wasmParams: [("value", .f64)],
328351
wasmReturnType: .f64,
329-
optionalUsesStackABI: false,
330-
optionalUsesSideChannelReturn: true,
352+
optionalConvention: .sideChannelReturn,
331353
accessorTransform: .identity,
332354
lowerMethod: .stackReturn
333355
)
@@ -336,8 +358,7 @@ extension BridgeType {
336358
wasmParams: [("bytes", .i32), ("length", .i32)],
337359
wasmReturnType: nil,
338360
importReturnType: .i32,
339-
optionalUsesStackABI: false,
340-
optionalUsesSideChannelReturn: true,
361+
optionalConvention: .sideChannelReturn,
341362
accessorTransform: .identity,
342363
lowerMethod: .stackReturn
343364
)
@@ -347,49 +368,43 @@ extension BridgeType {
347368
return BridgeTypeDescriptor(
348369
wasmParams: [("value", .i32)],
349370
wasmReturnType: .i32,
350-
optionalUsesStackABI: false,
351-
optionalUsesSideChannelReturn: true,
371+
optionalConvention: .sideChannelReturn,
352372
accessorTransform: transform,
353373
lowerMethod: .stackReturn
354374
)
355375
case .jsValue:
356376
return BridgeTypeDescriptor(
357377
wasmParams: [("kind", .i32), ("payload1", .i32), ("payload2", .f64)],
358378
wasmReturnType: nil,
359-
optionalUsesStackABI: false,
360379
accessorTransform: .identity,
361380
lowerMethod: .stackReturn
362381
)
363382
case .swiftHeapObject:
364383
return BridgeTypeDescriptor(
365384
wasmParams: [("pointer", .pointer)],
366385
wasmReturnType: .pointer,
367-
optionalUsesStackABI: false,
368386
accessorTransform: .identity,
369387
lowerMethod: .stackReturn
370388
)
371389
case .unsafePointer:
372390
return BridgeTypeDescriptor(
373391
wasmParams: [("pointer", .pointer)],
374392
wasmReturnType: .pointer,
375-
optionalUsesStackABI: false,
376393
accessorTransform: .identity,
377394
lowerMethod: .stackReturn
378395
)
379396
case .swiftProtocol(let protocolName):
380397
return BridgeTypeDescriptor(
381398
wasmParams: [("value", .i32)],
382399
wasmReturnType: .i32,
383-
optionalUsesStackABI: false,
384-
optionalUsesSideChannelReturn: true,
400+
optionalConvention: .sideChannelReturn,
385401
accessorTransform: .cast("Any\(protocolName)"),
386402
lowerMethod: .stackReturn
387403
)
388404
case .caseEnum:
389405
return BridgeTypeDescriptor(
390406
wasmParams: [("value", .i32)],
391407
wasmReturnType: .i32,
392-
optionalUsesStackABI: false,
393408
accessorTransform: .identity,
394409
lowerMethod: .stackReturn
395410
)
@@ -400,43 +415,38 @@ extension BridgeType {
400415
wasmParams: [("bytes", .i32), ("length", .i32)],
401416
wasmReturnType: nil,
402417
importReturnType: .some(.i32),
403-
optionalUsesStackABI: false,
404-
optionalUsesSideChannelReturn: true,
418+
optionalConvention: .sideChannelReturn,
405419
accessorTransform: .identity,
406420
lowerMethod: .stackReturn
407421
)
408422
case .float:
409423
return BridgeTypeDescriptor(
410424
wasmParams: [("value", .f32)],
411425
wasmReturnType: .f32,
412-
optionalUsesStackABI: false,
413-
optionalUsesSideChannelReturn: true,
426+
optionalConvention: .sideChannelReturn,
414427
accessorTransform: .identity,
415428
lowerMethod: .stackReturn
416429
)
417430
case .double:
418431
return BridgeTypeDescriptor(
419432
wasmParams: [("value", .f64)],
420433
wasmReturnType: .f64,
421-
optionalUsesStackABI: false,
422-
optionalUsesSideChannelReturn: true,
434+
optionalConvention: .sideChannelReturn,
423435
accessorTransform: .identity,
424436
lowerMethod: .stackReturn
425437
)
426438
case .bool:
427439
return BridgeTypeDescriptor(
428440
wasmParams: [("value", .i32)],
429441
wasmReturnType: .i32,
430-
optionalUsesStackABI: false,
431442
accessorTransform: .identity,
432443
lowerMethod: .stackReturn
433444
)
434445
case .int, .int32, .int64, .uint, .uint32, .uint64:
435446
return BridgeTypeDescriptor(
436447
wasmParams: [("value", .i32)],
437448
wasmReturnType: .i32,
438-
optionalUsesStackABI: false,
439-
optionalUsesSideChannelReturn: true,
449+
optionalConvention: .sideChannelReturn,
440450
accessorTransform: .identity,
441451
lowerMethod: .stackReturn
442452
)
@@ -446,7 +456,6 @@ extension BridgeType {
446456
wasmParams: [("caseId", .i32)],
447457
wasmReturnType: nil,
448458
importReturnType: .i32,
449-
optionalUsesStackABI: false,
450459
usesStackLifting: true,
451460
accessorTransform: .identity,
452461
lowerMethod: .pushParameter
@@ -455,15 +464,13 @@ extension BridgeType {
455464
return BridgeTypeDescriptor(
456465
wasmParams: [("funcRef", .i32)],
457466
wasmReturnType: .i32,
458-
optionalUsesStackABI: false,
459467
accessorTransform: .identity,
460468
lowerMethod: .stackReturn
461469
)
462470
case .swiftStruct:
463471
return BridgeTypeDescriptor(
464472
wasmParams: [],
465473
wasmReturnType: nil,
466-
optionalUsesStackABI: true,
467474
usesStackLifting: true,
468475
accessorTransform: .identity,
469476
lowerMethod: .fullReturn
@@ -472,7 +479,6 @@ extension BridgeType {
472479
return BridgeTypeDescriptor(
473480
wasmParams: [],
474481
wasmReturnType: nil,
475-
optionalUsesStackABI: true,
476482
usesStackLifting: true,
477483
accessorTransform: .identity,
478484
lowerMethod: .fullReturn
@@ -481,15 +487,13 @@ extension BridgeType {
481487
return BridgeTypeDescriptor(
482488
wasmParams: [],
483489
wasmReturnType: nil,
484-
optionalUsesStackABI: true,
485490
accessorTransform: .identity,
486491
lowerMethod: .fullReturn
487492
)
488493
case .void, .namespaceEnum:
489494
return BridgeTypeDescriptor(
490495
wasmParams: [],
491496
wasmReturnType: nil,
492-
optionalUsesStackABI: false,
493497
accessorTransform: .identity,
494498
lowerMethod: .none
495499
)
@@ -1473,7 +1477,7 @@ extension BridgeType {
14731477
guard case .nullable = self else {
14741478
return false
14751479
}
1476-
return descriptor.optionalUsesSideChannelReturn
1480+
return descriptor.optionalConvention == .sideChannelReturn
14771481
}
14781482
}
14791483

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -660,14 +660,16 @@ export async function createInstantiator(options, swift) {
660660
},
661661
processOptionalArray: function bjs_processOptionalArray(values) {
662662
const isSome = values != null;
663-
const valuesCleanups = [];
663+
let valuesCleanup;
664664
if (isSome) {
665665
const arrayCleanups = [];
666666
for (const elem of values) {
667667
i32Stack.push((elem | 0));
668668
}
669669
i32Stack.push(values.length);
670-
valuesCleanups.push(() => { for (const cleanup of arrayCleanups) { cleanup(); } });
670+
valuesCleanup = () => {
671+
for (const cleanup of arrayCleanups) { cleanup(); }
672+
};
671673
}
672674
i32Stack.push(+isSome);
673675
instance.exports.bjs_processOptionalArray();
@@ -685,7 +687,7 @@ export async function createInstantiator(options, swift) {
685687
} else {
686688
optResult = null;
687689
}
688-
for (const cleanup of valuesCleanups) { cleanup(); }
690+
if (valuesCleanup) { valuesCleanup(); }
689691
return optResult;
690692
},
691693
processOptionalPointArray: function bjs_processOptionalPointArray(points) {

0 commit comments

Comments
 (0)