Skip to content

Commit df5af2e

Browse files
committed
BridgeJS: Add importParams, NilSentinel, sentinel-based optional lowering, and collapse liftExpression
1 parent 3e66088 commit df5af2e

File tree

6 files changed

+131
-53
lines changed

6 files changed

+131
-53
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -717,13 +717,8 @@ struct StackCodegen {
717717
/// - Returns: An ExprSyntax representing the lift expression
718718
func liftExpression(for type: BridgeType) -> ExprSyntax {
719719
switch type {
720-
case .string, .int, .uint, .bool, .float, .double,
721-
.jsObject(nil), .jsValue, .swiftStruct, .swiftHeapObject, .unsafePointer,
722-
.swiftProtocol, .caseEnum, .associatedValueEnum, .rawValueEnum:
723-
return "\(raw: type.swiftType).bridgeJSLiftParameter()"
724720
case .jsObject(let className?):
725721
return "\(raw: className)(unsafelyWrapping: JSObject.bridgeJSLiftParameter())"
726-
727722
case .nullable(let wrappedType, let kind):
728723
return liftNullableExpression(wrappedType: wrappedType, kind: kind)
729724
case .array(let elementType):
@@ -734,6 +729,8 @@ struct StackCodegen {
734729
return "JSObject.bridgeJSLiftParameter()"
735730
case .void, .namespaceEnum:
736731
return "()"
732+
default:
733+
return "\(raw: type.swiftType).bridgeJSLiftParameter()"
737734
}
738735
}
739736

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -719,18 +719,7 @@ extension BridgeType {
719719
}
720720

721721
private func loweringParameterInfoForNonOptional(context: BridgeContext) throws -> LoweringParameterInfo {
722-
switch self {
723-
case .swiftStruct where context == .importTS:
724-
return LoweringParameterInfo(loweredParameters: [("objectId", .i32)])
725-
case .string:
726-
return LoweringParameterInfo(loweredParameters: [("value", .i32)])
727-
case .rawValueEnum(_, .string):
728-
return LoweringParameterInfo(loweredParameters: [("value", .i32)])
729-
default:
730-
break
731-
}
732-
733-
return LoweringParameterInfo(loweredParameters: descriptor.wasmParams)
722+
return LoweringParameterInfo(loweredParameters: descriptor.importParams)
734723
}
735724

736725
struct LiftingReturnInfo {
@@ -759,14 +748,6 @@ extension BridgeType {
759748
}
760749

761750
private func liftingReturnInfoForNonOptional(context: BridgeContext) -> LiftingReturnInfo {
762-
if context == .importTS {
763-
switch self {
764-
case .swiftStruct:
765-
return LiftingReturnInfo(valueToLift: .i32)
766-
default:
767-
break
768-
}
769-
}
770751
return LiftingReturnInfo(valueToLift: descriptor.importReturnType)
771752
}
772753
}

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,15 @@ struct IntrinsicJSFragment: Sendable {
11131113
)
11141114
}
11151115

1116+
if desc.nilSentinel.hasSentinel {
1117+
let innerFragment = try lowerReturn(type: wrappedType, context: .exportSwift)
1118+
return sentinelOptionalLowerReturn(
1119+
wrappedType: wrappedType,
1120+
kind: kind,
1121+
innerFragment: innerFragment
1122+
)
1123+
}
1124+
11161125
return IntrinsicJSFragment(
11171126
parameters: ["value"],
11181127
printCode: { arguments, context in
@@ -1123,8 +1132,6 @@ struct IntrinsicJSFragment: Sendable {
11231132
printer.write("const \(isSomeVar) = \(presenceExpr);")
11241133

11251134
switch wrappedType {
1126-
case .caseEnum:
1127-
printer.write("return \(isSomeVar) ? (\(value) | 0) : -1;")
11281135
case .string, .rawValueEnum(_, .string):
11291136
printer.write("if (\(isSomeVar)) {")
11301137
printer.indent {
@@ -1155,8 +1162,6 @@ struct IntrinsicJSFragment: Sendable {
11551162
scope.emitPushF64Parameter(payload2Var, printer: printer)
11561163
}
11571164
scope.emitPushI32Parameter("\(isSomeVar) ? 1 : 0", printer: printer)
1158-
case .swiftHeapObject:
1159-
printer.write("return \(isSomeVar) ? \(value).pointer : 0;")
11601165
case .array(let elementType):
11611166
printer.write("if (\(isSomeVar)) {")
11621167
try printer.indent {
@@ -1174,21 +1179,6 @@ struct IntrinsicJSFragment: Sendable {
11741179
}
11751180
printer.write("}")
11761181
scope.emitPushI32Parameter("\(isSomeVar) ? 1 : 0", printer: printer)
1177-
case .associatedValueEnum(let fullName):
1178-
let base = fullName.components(separatedBy: ".").last ?? fullName
1179-
let caseIdVar = scope.variable("caseId")
1180-
printer.write("if (\(isSomeVar)) {")
1181-
printer.indent {
1182-
printer.write(
1183-
"const { caseId: \(caseIdVar) } = \(JSGlueVariableScope.reservedEnumHelpers).\(base).lower(\(value));"
1184-
)
1185-
printer.write("return \(caseIdVar);")
1186-
}
1187-
printer.write("} else {")
1188-
printer.indent {
1189-
printer.write("return -1;")
1190-
}
1191-
printer.write("}")
11921182
case .dictionary(let valueType):
11931183
printer.write("if (\(isSomeVar)) {")
11941184
try printer.indent {
@@ -1217,6 +1207,81 @@ struct IntrinsicJSFragment: Sendable {
12171207
)
12181208
}
12191209

1210+
private static func sentinelOptionalLowerReturn(
1211+
wrappedType: BridgeType,
1212+
kind: JSOptionalKind,
1213+
innerFragment: IntrinsicJSFragment
1214+
) -> IntrinsicJSFragment {
1215+
let desc = wrappedType.descriptor
1216+
let sentinelLiteral = desc.nilSentinel.jsLiteral
1217+
1218+
return IntrinsicJSFragment(
1219+
parameters: ["value"],
1220+
printCode: { arguments, context in
1221+
let (scope, printer, cleanupCode) = (context.scope, context.printer, context.cleanupCode)
1222+
let value = arguments[0]
1223+
let isSomeVar = scope.variable("isSome")
1224+
let presenceExpr = kind.presenceCheck(value: value)
1225+
printer.write("const \(isSomeVar) = \(presenceExpr);")
1226+
1227+
let bufferPrinter = CodeFragmentPrinter()
1228+
let innerCleanup = CodeFragmentPrinter()
1229+
let innerResults = try innerFragment.printCode(
1230+
[value],
1231+
context.with(\.printer, bufferPrinter).with(\.cleanupCode, innerCleanup)
1232+
)
1233+
1234+
let hasSideEffects = !bufferPrinter.lines.isEmpty
1235+
let innerExpr = innerResults.first
1236+
1237+
let innerCleanupLines = innerCleanup.lines.filter {
1238+
!$0.trimmingCharacters(in: .whitespaces).isEmpty
1239+
}
1240+
if !innerCleanupLines.isEmpty {
1241+
let cleanupVar = scope.variable("\(value)Cleanup")
1242+
printer.write("let \(cleanupVar);")
1243+
printer.write("if (\(isSomeVar)) {")
1244+
printer.indent {
1245+
for line in bufferPrinter.lines {
1246+
printer.write(line)
1247+
}
1248+
printer.write("\(cleanupVar) = () => {")
1249+
printer.indent {
1250+
for line in innerCleanupLines {
1251+
printer.write(line)
1252+
}
1253+
}
1254+
printer.write("};")
1255+
}
1256+
printer.write("}")
1257+
if let expr = innerExpr {
1258+
printer.write("return \(isSomeVar) ? \(expr) : \(sentinelLiteral);")
1259+
}
1260+
cleanupCode.write("if (\(cleanupVar)) { \(cleanupVar)(); }")
1261+
} else if hasSideEffects {
1262+
printer.write("if (\(isSomeVar)) {")
1263+
printer.indent {
1264+
for line in bufferPrinter.lines {
1265+
printer.write(line)
1266+
}
1267+
if let expr = innerExpr {
1268+
printer.write("return \(expr);")
1269+
}
1270+
}
1271+
printer.write("} else {")
1272+
printer.indent {
1273+
printer.write("return \(sentinelLiteral);")
1274+
}
1275+
printer.write("}")
1276+
} else if let expr = innerExpr {
1277+
printer.write("return \(isSomeVar) ? \(expr) : \(sentinelLiteral);")
1278+
}
1279+
1280+
return []
1281+
}
1282+
)
1283+
}
1284+
12201285
// MARK: - Protocol Support
12211286

12221287
static func protocolPropertyOptionalToSideChannel(wrappedType: BridgeType) throws -> IntrinsicJSFragment {

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,33 @@ public enum OptionalConvention: Sendable, Equatable {
200200
case sideChannelReturn
201201
}
202202

203+
/// A bit pattern that is never a valid value for a type, usable to represent `nil`
204+
/// without an extra `isSome` flag. Inspired by Swift's "extra inhabitant" concept.
205+
///
206+
/// Types with a nil sentinel can encode Optional<T> in T's own return slot:
207+
/// the sentinel value means absent, any other value means present.
208+
/// Types without a sentinel need either an inline isSome flag or a side channel.
209+
public enum NilSentinel: Sendable, Equatable {
210+
/// No sentinel exists - all bit patterns are valid values.
211+
case none
212+
/// A specific i32 value is never valid (e.g. 0 for object IDs, -1 for enum tags).
213+
case i32(Int32)
214+
/// A null pointer (0) is the sentinel.
215+
case pointer
216+
217+
public var jsLiteral: String {
218+
switch self {
219+
case .none: fatalError("No sentinel value for .none")
220+
case .i32(let value): return "\(value)"
221+
case .pointer: return "0"
222+
}
223+
}
224+
225+
public var hasSentinel: Bool {
226+
self != .none
227+
}
228+
}
229+
203230
/// Captures the WASM ABI shape for a ``BridgeType`` so codegen can read descriptor fields
204231
/// instead of switching on every concrete type.
205232
///
@@ -208,33 +235,32 @@ public enum OptionalConvention: Sendable, Equatable {
208235
/// import side returns a pointer/ID while the export side uses the stack.
209236
public struct BridgeTypeDescriptor: Sendable {
210237
public let wasmParams: [(name: String, type: WasmCoreType)]
238+
public let importParams: [(name: String, type: WasmCoreType)]
211239
public let wasmReturnType: WasmCoreType?
212240
public let importReturnType: WasmCoreType?
213241
public let optionalConvention: OptionalConvention
242+
public let nilSentinel: NilSentinel
214243
public let usesStackLifting: Bool
215244
public let accessorTransform: AccessorTransform
216245
public let lowerMethod: LowerMethod
217246

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.
225247
public init(
226248
wasmParams: [(name: String, type: WasmCoreType)],
249+
importParams: [(name: String, type: WasmCoreType)]? = nil,
227250
wasmReturnType: WasmCoreType?,
228251
importReturnType: WasmCoreType?? = nil,
229252
optionalConvention: OptionalConvention? = nil,
253+
nilSentinel: NilSentinel = .none,
230254
usesStackLifting: Bool = false,
231255
accessorTransform: AccessorTransform,
232256
lowerMethod: LowerMethod
233257
) {
234258
self.wasmParams = wasmParams
259+
self.importParams = importParams ?? wasmParams
235260
self.wasmReturnType = wasmReturnType
236261
self.importReturnType = importReturnType ?? wasmReturnType
237262
self.optionalConvention = optionalConvention ?? (wasmParams.isEmpty ? .stackABI : .inlineFlag)
263+
self.nilSentinel = nilSentinel
238264
self.usesStackLifting = usesStackLifting
239265
self.accessorTransform = accessorTransform
240266
self.lowerMethod = lowerMethod
@@ -356,6 +382,7 @@ extension BridgeType {
356382
case .string:
357383
return BridgeTypeDescriptor(
358384
wasmParams: [("bytes", .i32), ("length", .i32)],
385+
importParams: [("value", .i32)],
359386
wasmReturnType: nil,
360387
importReturnType: .i32,
361388
optionalConvention: .sideChannelReturn,
@@ -369,6 +396,7 @@ extension BridgeType {
369396
wasmParams: [("value", .i32)],
370397
wasmReturnType: .i32,
371398
optionalConvention: .sideChannelReturn,
399+
nilSentinel: .i32(0),
372400
accessorTransform: transform,
373401
lowerMethod: .stackReturn
374402
)
@@ -383,6 +411,7 @@ extension BridgeType {
383411
return BridgeTypeDescriptor(
384412
wasmParams: [("pointer", .pointer)],
385413
wasmReturnType: .pointer,
414+
nilSentinel: .pointer,
386415
accessorTransform: .identity,
387416
lowerMethod: .stackReturn
388417
)
@@ -398,13 +427,15 @@ extension BridgeType {
398427
wasmParams: [("value", .i32)],
399428
wasmReturnType: .i32,
400429
optionalConvention: .sideChannelReturn,
430+
nilSentinel: .i32(0),
401431
accessorTransform: .cast("Any\(protocolName)"),
402432
lowerMethod: .stackReturn
403433
)
404434
case .caseEnum:
405435
return BridgeTypeDescriptor(
406436
wasmParams: [("value", .i32)],
407437
wasmReturnType: .i32,
438+
nilSentinel: .i32(-1),
408439
accessorTransform: .identity,
409440
lowerMethod: .stackReturn
410441
)
@@ -413,6 +444,7 @@ extension BridgeType {
413444
case .string:
414445
return BridgeTypeDescriptor(
415446
wasmParams: [("bytes", .i32), ("length", .i32)],
447+
importParams: [("value", .i32)],
416448
wasmReturnType: nil,
417449
importReturnType: .some(.i32),
418450
optionalConvention: .sideChannelReturn,
@@ -456,6 +488,7 @@ extension BridgeType {
456488
wasmParams: [("caseId", .i32)],
457489
wasmReturnType: nil,
458490
importReturnType: .i32,
491+
nilSentinel: .i32(-1),
459492
usesStackLifting: true,
460493
accessorTransform: .identity,
461494
lowerMethod: .pushParameter
@@ -470,7 +503,9 @@ extension BridgeType {
470503
case .swiftStruct:
471504
return BridgeTypeDescriptor(
472505
wasmParams: [],
506+
importParams: [("objectId", .i32)],
473507
wasmReturnType: nil,
508+
importReturnType: .i32,
474509
usesStackLifting: true,
475510
accessorTransform: .identity,
476511
lowerMethod: .fullReturn

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ export async function createInstantiator(options, swift) {
428428
try {
429429
let ret = swift.memory.getObject(self).directionOptional;
430430
const isSome = ret != null;
431-
return isSome ? (ret | 0) : -1;
431+
return isSome ? ret : -1;
432432
} catch (error) {
433433
setException(error);
434434
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ export async function createInstantiator(options, swift) {
745745
const callback = swift.memory.getObject(callbackId);
746746
let ret = callback(param0IsSome ? param0WrappedValue : null);
747747
const isSome = ret != null;
748-
return isSome ? (ret | 0) : -1;
748+
return isSome ? ret : -1;
749749
} catch (error) {
750750
setException(error);
751751
}

0 commit comments

Comments
 (0)