Skip to content

Commit 3c8726e

Browse files
Merge pull request #417 from swiftwasm/yt/support-class-property
BridgeJS: Add property support
2 parents 79a9c1b + 8cedac7 commit 3c8726e

File tree

20 files changed

+2570
-79
lines changed

20 files changed

+2570
-79
lines changed

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
}
4848
],
4949
"name" : "PlayBridgeJS",
50+
"properties" : [
51+
52+
],
5053
"swiftCallName" : "PlayBridgeJS"
5154
},
5255
{
@@ -117,6 +120,9 @@
117120
}
118121
],
119122
"name" : "PlayBridgeJSOutput",
123+
"properties" : [
124+
125+
],
120126
"swiftCallName" : "PlayBridgeJSOutput"
121127
}
122128
],

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 146 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,75 @@ public class ExportSwift {
332332
return .skipChildren
333333
}
334334

335+
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
336+
guard node.attributes.hasJSAttribute() else { return .skipChildren }
337+
guard case .classBody(let className, let classKey) = state else {
338+
diagnose(node: node, message: "@JS var must be inside a @JS class")
339+
return .skipChildren
340+
}
341+
342+
if let jsAttribute = node.attributes.firstJSAttribute,
343+
extractNamespace(from: jsAttribute) != nil
344+
{
345+
diagnose(
346+
node: jsAttribute,
347+
message: "Namespace is not supported for property declarations",
348+
hint: "Remove the namespace from @JS attribute"
349+
)
350+
}
351+
352+
// Process each binding (variable declaration)
353+
for binding in node.bindings {
354+
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
355+
diagnose(node: binding.pattern, message: "Complex patterns not supported for @JS properties")
356+
continue
357+
}
358+
359+
let propertyName = pattern.identifier.text
360+
361+
guard let typeAnnotation = binding.typeAnnotation else {
362+
diagnose(node: binding, message: "@JS property must have explicit type annotation")
363+
continue
364+
}
365+
366+
guard let propertyType = self.parent.lookupType(for: typeAnnotation.type) else {
367+
diagnoseUnsupportedType(node: typeAnnotation.type, type: typeAnnotation.type.trimmedDescription)
368+
continue
369+
}
370+
371+
// Check if property is readonly
372+
let isLet = node.bindingSpecifier.tokenKind == .keyword(.let)
373+
let isGetterOnly = node.bindings.contains(where: {
374+
switch $0.accessorBlock?.accessors {
375+
case .accessors(let accessors):
376+
// Has accessors - check if it only has a getter (no setter, willSet, or didSet)
377+
return !accessors.contains(where: { accessor in
378+
let tokenKind = accessor.accessorSpecifier.tokenKind
379+
return tokenKind == .keyword(.set) || tokenKind == .keyword(.willSet)
380+
|| tokenKind == .keyword(.didSet)
381+
})
382+
case .getter:
383+
// Has only a getter block
384+
return true
385+
case nil:
386+
// No accessor block - this is a stored property, not readonly
387+
return false
388+
}
389+
})
390+
let isReadonly = isLet || isGetterOnly
391+
392+
let exportedProperty = ExportedProperty(
393+
name: propertyName,
394+
type: propertyType,
395+
isReadonly: isReadonly
396+
)
397+
398+
exportedClassByName[classKey]?.properties.append(exportedProperty)
399+
}
400+
401+
return .skipChildren
402+
}
403+
335404
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
336405
let name = node.name.text
337406

@@ -359,6 +428,7 @@ public class ExportSwift {
359428
swiftCallName: swiftCallName,
360429
constructor: nil,
361430
methods: [],
431+
properties: [],
362432
namespace: effectiveNamespace
363433
)
364434
let uniqueKey = classKey(name: name, namespace: effectiveNamespace)
@@ -689,7 +759,8 @@ public class ExportSwift {
689759

690760
class ExportedThunkBuilder {
691761
var body: [CodeBlockItemSyntax] = []
692-
var abiParameterForwardings: [LabeledExprSyntax] = []
762+
var liftedParameterExprs: [ExprSyntax] = []
763+
var parameters: [Parameter] = []
693764
var abiParameterSignatures: [(name: String, type: WasmCoreType)] = []
694765
var abiReturnType: WasmCoreType?
695766
let effects: Effects
@@ -708,38 +779,19 @@ public class ExportSwift {
708779
}
709780

710781
func liftParameter(param: Parameter) {
782+
parameters.append(param)
711783
switch param.type {
712784
case .bool:
713-
abiParameterForwardings.append(
714-
LabeledExprSyntax(
715-
label: param.label,
716-
expression: ExprSyntax("\(raw: param.name) == 1")
717-
)
718-
)
785+
liftedParameterExprs.append(ExprSyntax("\(raw: param.name) == 1"))
719786
abiParameterSignatures.append((param.name, .i32))
720787
case .int:
721-
abiParameterForwardings.append(
722-
LabeledExprSyntax(
723-
label: param.label,
724-
expression: ExprSyntax("\(raw: param.type.swiftType)(\(raw: param.name))")
725-
)
726-
)
788+
liftedParameterExprs.append(ExprSyntax("\(raw: param.type.swiftType)(\(raw: param.name))"))
727789
abiParameterSignatures.append((param.name, .i32))
728790
case .float:
729-
abiParameterForwardings.append(
730-
LabeledExprSyntax(
731-
label: param.label,
732-
expression: ExprSyntax("\(raw: param.name)")
733-
)
734-
)
791+
liftedParameterExprs.append(ExprSyntax("\(raw: param.name)"))
735792
abiParameterSignatures.append((param.name, .f32))
736793
case .double:
737-
abiParameterForwardings.append(
738-
LabeledExprSyntax(
739-
label: param.label,
740-
expression: ExprSyntax("\(raw: param.name)")
741-
)
742-
)
794+
liftedParameterExprs.append(ExprSyntax("\(raw: param.name)"))
743795
abiParameterSignatures.append((param.name, .f64))
744796
case .string:
745797
let bytesLabel = "\(param.name)Bytes"
@@ -751,21 +803,11 @@ public class ExportSwift {
751803
}
752804
"""
753805
append(prepare)
754-
abiParameterForwardings.append(
755-
LabeledExprSyntax(
756-
label: param.label,
757-
expression: ExprSyntax("\(raw: param.name)")
758-
)
759-
)
806+
liftedParameterExprs.append(ExprSyntax("\(raw: param.name)"))
760807
abiParameterSignatures.append((bytesLabel, .i32))
761808
abiParameterSignatures.append((lengthLabel, .i32))
762809
case .caseEnum(let enumName):
763-
abiParameterForwardings.append(
764-
LabeledExprSyntax(
765-
label: param.label,
766-
expression: ExprSyntax("\(raw: enumName)(bridgeJSRawValue: \(raw: param.name))!")
767-
)
768-
)
810+
liftedParameterExprs.append(ExprSyntax("\(raw: enumName)(bridgeJSRawValue: \(raw: param.name))!"))
769811
abiParameterSignatures.append((param.name, .i32))
770812
case .rawValueEnum(let enumName, let rawType):
771813
if rawType == .string {
@@ -778,12 +820,7 @@ public class ExportSwift {
778820
}
779821
"""
780822
append(prepare)
781-
abiParameterForwardings.append(
782-
LabeledExprSyntax(
783-
label: param.label,
784-
expression: ExprSyntax("\(raw: enumName)(rawValue: \(raw: param.name))!")
785-
)
786-
)
823+
liftedParameterExprs.append(ExprSyntax("\(raw: enumName)(rawValue: \(raw: param.name))!"))
787824
abiParameterSignatures.append((bytesLabel, .i32))
788825
abiParameterSignatures.append((lengthLabel, .i32))
789826
} else {
@@ -802,12 +839,7 @@ public class ExportSwift {
802839
conversionExpr = "\(enumName)(rawValue: \(rawType.rawValue)(\(param.name)))!"
803840
}
804841

805-
abiParameterForwardings.append(
806-
LabeledExprSyntax(
807-
label: param.label,
808-
expression: ExprSyntax(stringLiteral: conversionExpr)
809-
)
810-
)
842+
liftedParameterExprs.append(ExprSyntax(stringLiteral: conversionExpr))
811843
if let wasmType = rawType.wasmCoreType {
812844
abiParameterSignatures.append((param.name, wasmType))
813845
}
@@ -817,36 +849,35 @@ public class ExportSwift {
817849
case .namespaceEnum:
818850
break
819851
case .jsObject(nil):
820-
abiParameterForwardings.append(
821-
LabeledExprSyntax(
822-
label: param.label,
823-
expression: ExprSyntax("JSObject(id: UInt32(bitPattern: \(raw: param.name)))")
824-
)
825-
)
852+
liftedParameterExprs.append(ExprSyntax("JSObject(id: UInt32(bitPattern: \(raw: param.name)))"))
826853
abiParameterSignatures.append((param.name, .i32))
827854
case .jsObject(let name):
828-
abiParameterForwardings.append(
829-
LabeledExprSyntax(
830-
label: param.label,
831-
expression: ExprSyntax("\(raw: name)(takingThis: UInt32(bitPattern: \(raw: param.name)))")
832-
)
855+
liftedParameterExprs.append(
856+
ExprSyntax("\(raw: name)(takingThis: UInt32(bitPattern: \(raw: param.name)))")
833857
)
834858
abiParameterSignatures.append((param.name, .i32))
835859
case .swiftHeapObject:
836860
let objectExpr: ExprSyntax =
837861
"Unmanaged<\(raw: param.type.swiftType)>.fromOpaque(\(raw: param.name)).takeUnretainedValue()"
838-
abiParameterForwardings.append(
839-
LabeledExprSyntax(label: param.label, expression: objectExpr)
840-
)
862+
liftedParameterExprs.append(objectExpr)
841863
abiParameterSignatures.append((param.name, .pointer))
842864
case .void:
843865
break
844866
}
845867
}
846868

869+
private func removeFirstLiftedParameter() -> (parameter: Parameter, expr: ExprSyntax) {
870+
let parameter = parameters.removeFirst()
871+
let expr = liftedParameterExprs.removeFirst()
872+
return (parameter, expr)
873+
}
874+
847875
private func renderCallStatement(callee: ExprSyntax, returnType: BridgeType) -> CodeBlockItemSyntax {
876+
let labeledParams = zip(parameters, liftedParameterExprs).map { param, expr in
877+
LabeledExprSyntax(label: param.label, expression: expr)
878+
}
848879
var callExpr: ExprSyntax =
849-
"\(raw: callee)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
880+
"\(raw: callee)(\(raw: labeledParams.map { $0.description }.joined(separator: ", ")))"
850881
if effects.isAsync {
851882
callExpr = ExprSyntax(
852883
AwaitExprSyntax(awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), expression: callExpr)
@@ -884,14 +915,30 @@ public class ExportSwift {
884915
}
885916

886917
func callMethod(klassName: String, methodName: String, returnType: BridgeType) {
887-
let _selfParam = self.abiParameterForwardings.removeFirst()
918+
let (_, selfExpr) = removeFirstLiftedParameter()
888919
let item = renderCallStatement(
889-
callee: "\(raw: _selfParam).\(raw: methodName)",
920+
callee: "\(raw: selfExpr).\(raw: methodName)",
890921
returnType: returnType
891922
)
892923
append(item)
893924
}
894925

926+
func callPropertyGetter(klassName: String, propertyName: String, returnType: BridgeType) {
927+
let (_, selfExpr) = removeFirstLiftedParameter()
928+
let retMutability = returnType == .string ? "var" : "let"
929+
if returnType == .void {
930+
append("\(raw: selfExpr).\(raw: propertyName)")
931+
} else {
932+
append("\(raw: retMutability) ret = \(raw: selfExpr).\(raw: propertyName)")
933+
}
934+
}
935+
936+
func callPropertySetter(klassName: String, propertyName: String) {
937+
let (_, selfExpr) = removeFirstLiftedParameter()
938+
let (_, newValueExpr) = removeFirstLiftedParameter()
939+
append("\(raw: selfExpr).\(raw: propertyName) = \(raw: newValueExpr)")
940+
}
941+
895942
func lowerReturnValue(returnType: BridgeType) {
896943
if effects.isAsync {
897944
// Async functions always return a Promise, which is a JSObject
@@ -1157,6 +1204,39 @@ public class ExportSwift {
11571204
decls.append(builder.render(abiName: method.abiName))
11581205
}
11591206

1207+
// Generate property getters and setters
1208+
for property in klass.properties {
1209+
// Generate getter
1210+
let getterBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
1211+
getterBuilder.liftParameter(
1212+
param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name))
1213+
)
1214+
getterBuilder.callPropertyGetter(
1215+
klassName: klass.name,
1216+
propertyName: property.name,
1217+
returnType: property.type
1218+
)
1219+
getterBuilder.lowerReturnValue(returnType: property.type)
1220+
decls.append(getterBuilder.render(abiName: property.getterAbiName(className: klass.name)))
1221+
1222+
// Generate setter if property is not readonly
1223+
if !property.isReadonly {
1224+
let setterBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
1225+
setterBuilder.liftParameter(
1226+
param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name))
1227+
)
1228+
setterBuilder.liftParameter(
1229+
param: Parameter(label: "value", name: "value", type: property.type)
1230+
)
1231+
setterBuilder.callPropertySetter(
1232+
klassName: klass.name,
1233+
propertyName: property.name
1234+
)
1235+
setterBuilder.lowerReturnValue(returnType: .void)
1236+
decls.append(setterBuilder.render(abiName: property.setterAbiName(className: klass.name)))
1237+
}
1238+
}
1239+
11601240
do {
11611241
decls.append(
11621242
"""

0 commit comments

Comments
 (0)