Skip to content

Commit 395ca24

Browse files
committed
BridgeJS: Basic support for properties in protocols
1 parent 6310307 commit 395ca24

File tree

12 files changed

+470
-49
lines changed

12 files changed

+470
-49
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 198 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -749,9 +749,8 @@ public class ExportSwift {
749749
case .topLevel:
750750
diagnose(node: node, message: "@JS var must be inside a @JS class or enum")
751751
return .skipChildren
752-
case .protocolBody(_, _):
753-
diagnose(node: node, message: "Properties are not supported in protocols")
754-
return .skipChildren
752+
case .protocolBody(let protocolName, let protocolKey):
753+
return visitProtocolProperty(node: node, protocolName: protocolName, protocolKey: protocolKey)
755754
}
756755

757756
// Process each binding (variable declaration)
@@ -775,23 +774,8 @@ public class ExportSwift {
775774

776775
// Check if property is readonly
777776
let isLet = node.bindingSpecifier.tokenKind == .keyword(.let)
778-
let isGetterOnly = node.bindings.contains(where: {
779-
switch $0.accessorBlock?.accessors {
780-
case .accessors(let accessors):
781-
// Has accessors - check if it only has a getter (no setter, willSet, or didSet)
782-
return !accessors.contains(where: { accessor in
783-
let tokenKind = accessor.accessorSpecifier.tokenKind
784-
return tokenKind == .keyword(.set) || tokenKind == .keyword(.willSet)
785-
|| tokenKind == .keyword(.didSet)
786-
})
787-
case .getter:
788-
// Has only a getter block
789-
return true
790-
case nil:
791-
// No accessor block - this is a stored property, not readonly
792-
return false
793-
}
794-
})
777+
let isGetterOnly = node.bindings.contains(where: { self.hasOnlyGetter($0.accessorBlock) })
778+
795779
let isReadonly = isLet || isGetterOnly
796780

797781
let exportedProperty = ExportedProperty(
@@ -997,6 +981,17 @@ public class ExportSwift {
997981
message: "Protocol visibility must be at least internal"
998982
)
999983

984+
let protocolUniqueKey = makeKey(name: name, namespace: namespaceResult.namespace)
985+
986+
exportedProtocolByName[protocolUniqueKey] = ExportedProtocol(
987+
name: name,
988+
methods: [],
989+
properties: [],
990+
namespace: namespaceResult.namespace
991+
)
992+
993+
stateStack.push(state: .protocolBody(name: name, key: protocolUniqueKey))
994+
1000995
var methods: [ExportedFunction] = []
1001996
for member in node.memberBlock.members {
1002997
if let funcDecl = member.decl.as(FunctionDeclSyntax.self) {
@@ -1007,18 +1002,22 @@ public class ExportSwift {
10071002
) {
10081003
methods.append(exportedFunction)
10091004
}
1005+
} else if let varDecl = member.decl.as(VariableDeclSyntax.self) {
1006+
_ = visitProtocolProperty(node: varDecl, protocolName: name, protocolKey: protocolUniqueKey)
10101007
}
10111008
}
10121009

10131010
let exportedProtocol = ExportedProtocol(
10141011
name: name,
10151012
methods: methods,
1013+
properties: exportedProtocolByName[protocolUniqueKey]?.properties ?? [],
10161014
namespace: namespaceResult.namespace
10171015
)
1018-
1019-
let protocolUniqueKey = makeKey(name: name, namespace: namespaceResult.namespace)
1016+
10201017
exportedProtocolByName[protocolUniqueKey] = exportedProtocol
10211018
exportedProtocolNames.append(protocolUniqueKey)
1019+
1020+
stateStack.pop()
10221021

10231022
parent.exportedProtocolNameByKey[protocolUniqueKey] = name
10241023

@@ -1075,6 +1074,89 @@ public class ExportSwift {
10751074
)
10761075
}
10771076

1077+
private func visitProtocolProperty(
1078+
node: VariableDeclSyntax,
1079+
protocolName: String,
1080+
protocolKey: String
1081+
) -> SyntaxVisitorContinueKind {
1082+
for binding in node.bindings {
1083+
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
1084+
diagnose(node: binding.pattern, message: "Complex patterns not supported for protocol properties")
1085+
continue
1086+
}
1087+
1088+
let propertyName = pattern.identifier.text
1089+
1090+
guard let typeAnnotation = binding.typeAnnotation else {
1091+
diagnose(node: binding, message: "Protocol property must have explicit type annotation")
1092+
continue
1093+
}
1094+
1095+
guard let propertyType = self.parent.lookupType(for: typeAnnotation.type) else {
1096+
diagnoseUnsupportedType(node: typeAnnotation.type, type: typeAnnotation.type.trimmedDescription)
1097+
continue
1098+
}
1099+
1100+
if case .optional = propertyType {
1101+
diagnose(
1102+
node: typeAnnotation.type,
1103+
message: "Optional properties are not yet supported in protocols"
1104+
)
1105+
continue
1106+
}
1107+
1108+
guard let accessorBlock = binding.accessorBlock else {
1109+
diagnose(
1110+
node: binding,
1111+
message: "Protocol property must specify { get } or { get set }",
1112+
hint: "Add { get } for readonly or { get set } for readwrite property"
1113+
)
1114+
continue
1115+
}
1116+
1117+
let isReadonly = hasOnlyGetter(accessorBlock)
1118+
1119+
let exportedProperty = ExportedProtocolProperty(
1120+
name: propertyName,
1121+
type: propertyType,
1122+
isReadonly: isReadonly
1123+
)
1124+
1125+
if var currentProtocol = exportedProtocolByName[protocolKey] {
1126+
var properties = currentProtocol.properties
1127+
properties.append(exportedProperty)
1128+
1129+
currentProtocol = ExportedProtocol(
1130+
name: currentProtocol.name,
1131+
methods: currentProtocol.methods,
1132+
properties: properties,
1133+
namespace: currentProtocol.namespace
1134+
)
1135+
exportedProtocolByName[protocolKey] = currentProtocol
1136+
}
1137+
}
1138+
1139+
return .skipChildren
1140+
}
1141+
1142+
private func hasOnlyGetter(_ accessorBlock: AccessorBlockSyntax?) -> Bool {
1143+
switch accessorBlock?.accessors {
1144+
case .accessors(let accessors):
1145+
// Has accessors - check if it only has a getter (no setter, willSet, or didSet)
1146+
return !accessors.contains(where: { accessor in
1147+
let tokenKind = accessor.accessorSpecifier.tokenKind
1148+
return tokenKind == .keyword(.set) || tokenKind == .keyword(.willSet)
1149+
|| tokenKind == .keyword(.didSet)
1150+
})
1151+
case .getter:
1152+
// Has only a getter block
1153+
return true
1154+
case nil:
1155+
// No accessor block - this is a stored property, not readonly
1156+
return false
1157+
}
1158+
}
1159+
10781160
override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind {
10791161
guard case .enumBody(_, let enumKey) = stateStack.current else {
10801162
return .visitChildren
@@ -2179,18 +2261,111 @@ public class ExportSwift {
21792261
methodDecls.append(methodImplementation)
21802262
}
21812263

2264+
var propertyDecls: [DeclSyntax] = []
2265+
2266+
for property in proto.properties {
2267+
let propertyImpl = try renderProtocolProperty(
2268+
property: property,
2269+
protocolName: protocolName,
2270+
moduleName: moduleName
2271+
)
2272+
propertyDecls.append(propertyImpl)
2273+
}
2274+
2275+
let allDecls = (methodDecls + propertyDecls).map { $0.description }.joined(separator: "\n\n")
2276+
21822277
return """
21832278
struct \(raw: wrapperName): \(raw: protocolName), _BridgedSwiftProtocolWrapper {
21842279
let jsObject: JSObject
21852280
2186-
\(raw: methodDecls.map { $0.description }.joined(separator: "\n\n"))
2281+
\(raw: allDecls)
21872282
21882283
static func bridgeJSLiftParameter(_ value: Int32) -> Self {
21892284
return \(raw: wrapperName)(jsObject: JSObject(id: UInt32(bitPattern: value)))
21902285
}
21912286
}
21922287
"""
21932288
}
2289+
2290+
private func renderProtocolProperty(
2291+
property: ExportedProtocolProperty,
2292+
protocolName: String,
2293+
moduleName: String
2294+
) throws -> DeclSyntax {
2295+
let getterAbiName = ABINameGenerator.generateABIName(
2296+
baseName: property.name,
2297+
namespace: nil,
2298+
staticContext: nil,
2299+
operation: "get",
2300+
className: protocolName
2301+
)
2302+
let setterAbiName = ABINameGenerator.generateABIName(
2303+
baseName: property.name,
2304+
namespace: nil,
2305+
staticContext: nil,
2306+
operation: "set",
2307+
className: protocolName
2308+
)
2309+
2310+
// Generate getter
2311+
let liftingInfo = try property.type.liftingReturnInfo()
2312+
let getterReturnType: String
2313+
let getterCallCode: String
2314+
2315+
if let abiType = liftingInfo.valueToLift {
2316+
getterReturnType = " -> \(abiType.swiftType)"
2317+
getterCallCode = """
2318+
let ret = _extern_get(this: Int32(bitPattern: jsObject.id))
2319+
return \(property.type.swiftType).bridgeJSLiftReturn(ret)
2320+
"""
2321+
} else {
2322+
// For String and other types that use tmpRetString
2323+
getterReturnType = ""
2324+
getterCallCode = """
2325+
_extern_get(this: Int32(bitPattern: jsObject.id))
2326+
return \(property.type.swiftType).bridgeJSLiftReturn()
2327+
"""
2328+
}
2329+
2330+
if property.isReadonly {
2331+
// Readonly property - only getter
2332+
return """
2333+
var \(raw: property.name): \(raw: property.type.swiftType) {
2334+
get {
2335+
@_extern(wasm, module: "\(raw: moduleName)", name: "\(raw: getterAbiName)")
2336+
func _extern_get(this: Int32)\(raw: getterReturnType)
2337+
\(raw: getterCallCode)
2338+
}
2339+
}
2340+
"""
2341+
} else {
2342+
// Readwrite property - getter and setter
2343+
let loweringInfo = try property.type.loweringParameterInfo()
2344+
assert(
2345+
loweringInfo.loweredParameters.count == 1,
2346+
"Protocol property setters must lower to a single WASM parameter"
2347+
)
2348+
2349+
let (paramName, wasmType) = loweringInfo.loweredParameters[0]
2350+
let setterParams = "this: Int32, \(paramName): \(wasmType.swiftType)"
2351+
let setterCallArgs = "this: Int32(bitPattern: jsObject.id), \(paramName): newValue.bridgeJSLowerParameter()"
2352+
2353+
return """
2354+
var \(raw: property.name): \(raw: property.type.swiftType) {
2355+
get {
2356+
@_extern(wasm, module: "\(raw: moduleName)", name: "\(raw: getterAbiName)")
2357+
func _extern_get(this: Int32)\(raw: getterReturnType)
2358+
\(raw: getterCallCode)
2359+
}
2360+
set {
2361+
@_extern(wasm, module: "\(raw: moduleName)", name: "\(raw: setterAbiName)")
2362+
func _extern_set(\(raw: setterParams))
2363+
_extern_set(\(raw: setterCallArgs))
2364+
}
2365+
}
2366+
"""
2367+
}
2368+
}
21942369
}
21952370

21962371
fileprivate enum Constants {

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,13 @@ struct BridgeJSLink {
314314
}
315315

316316
for proto in skeleton.protocols {
317+
for property in proto.properties {
318+
try renderProtocolProperty(
319+
importObjectBuilder: importObjectBuilder,
320+
protocol: proto,
321+
property: property
322+
)
323+
}
317324
for method in proto.methods {
318325
try renderProtocolMethod(
319326
importObjectBuilder: importObjectBuilder,
@@ -607,6 +614,13 @@ struct BridgeJSLink {
607614
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
608615
)
609616
}
617+
for property in proto.properties {
618+
let propertySignature =
619+
property.isReadonly
620+
? "readonly \(property.name): \(property.type.tsType);"
621+
: "\(property.name): \(property.type.tsType);"
622+
printer.write(propertySignature)
623+
}
610624
}
611625
printer.write("}")
612626
printer.nextLine()
@@ -2417,6 +2431,52 @@ extension BridgeJSLink {
24172431
return (funcLines, [])
24182432
}
24192433

2434+
func renderProtocolProperty(
2435+
importObjectBuilder: ImportObjectBuilder,
2436+
protocol: ExportedProtocol,
2437+
property: ExportedProtocolProperty
2438+
) throws {
2439+
let getterAbiName = ABINameGenerator.generateABIName(
2440+
baseName: property.name,
2441+
namespace: nil,
2442+
staticContext: nil,
2443+
operation: "get",
2444+
className: `protocol`.name
2445+
)
2446+
2447+
let getterThunkBuilder = ImportedThunkBuilder()
2448+
getterThunkBuilder.liftSelf()
2449+
let returnExpr = try getterThunkBuilder.callPropertyGetter(name: property.name, returnType: property.type)
2450+
let getterLines = getterThunkBuilder.renderFunction(
2451+
name: getterAbiName,
2452+
returnExpr: returnExpr,
2453+
returnType: property.type
2454+
)
2455+
importObjectBuilder.assignToImportObject(name: getterAbiName, function: getterLines)
2456+
2457+
if !property.isReadonly {
2458+
let setterAbiName = ABINameGenerator.generateABIName(
2459+
baseName: property.name,
2460+
namespace: nil,
2461+
staticContext: nil,
2462+
operation: "set",
2463+
className: `protocol`.name
2464+
)
2465+
let setterThunkBuilder = ImportedThunkBuilder()
2466+
setterThunkBuilder.liftSelf()
2467+
try setterThunkBuilder.liftParameter(
2468+
param: Parameter(label: nil, name: "value", type: property.type)
2469+
)
2470+
setterThunkBuilder.callPropertySetter(name: property.name, returnType: property.type)
2471+
let setterLines = setterThunkBuilder.renderFunction(
2472+
name: setterAbiName,
2473+
returnExpr: nil,
2474+
returnType: .void
2475+
)
2476+
importObjectBuilder.assignToImportObject(name: setterAbiName, function: setterLines)
2477+
}
2478+
}
2479+
24202480
func renderProtocolMethod(
24212481
importObjectBuilder: ImportObjectBuilder,
24222482
protocol: ExportedProtocol,

0 commit comments

Comments
 (0)