@@ -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
21962371fileprivate enum Constants {
0 commit comments