@@ -272,6 +272,75 @@ public class ExportSwift {
272272 return .skipChildren
273273 }
274274
275+ override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
276+ guard node.attributes.hasJSAttribute() else { return .skipChildren }
277+ guard case .classBody(let className) = state else {
278+ diagnose(node: node, message: "@JS var must be inside a @JS class")
279+ return .skipChildren
280+ }
281+
282+ if let jsAttribute = node.attributes.firstJSAttribute,
283+ extractNamespace(from: jsAttribute) != nil
284+ {
285+ diagnose(
286+ node: jsAttribute,
287+ message: "Namespace is not supported for property declarations",
288+ hint: "Remove the namespace from @JS attribute"
289+ )
290+ }
291+
292+ // Process each binding (variable declaration)
293+ for binding in node.bindings {
294+ guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
295+ diagnose(node: binding.pattern, message: "Complex patterns not supported for @JS properties")
296+ continue
297+ }
298+
299+ let propertyName = pattern.identifier.text
300+
301+ guard let typeAnnotation = binding.typeAnnotation else {
302+ diagnose(node: binding, message: "@JS property must have explicit type annotation")
303+ continue
304+ }
305+
306+ guard let propertyType = self.parent.lookupType(for: typeAnnotation.type) else {
307+ diagnoseUnsupportedType(node: typeAnnotation.type, type: typeAnnotation.type.trimmedDescription)
308+ continue
309+ }
310+
311+ // Check if property is readonly
312+ let isLet = node.bindingSpecifier.tokenKind == .keyword(.let)
313+ let isGetterOnly = node.bindings.contains(where: {
314+ switch $0.accessorBlock?.accessors {
315+ case .accessors(let accessors):
316+ // Has accessors - check if it only has a getter (no setter, willSet, or didSet)
317+ return !accessors.contains(where: { accessor in
318+ let tokenKind = accessor.accessorSpecifier.tokenKind
319+ return tokenKind == .keyword(.set) || tokenKind == .keyword(.willSet)
320+ || tokenKind == .keyword(.didSet)
321+ })
322+ case .getter:
323+ // Has only a getter block
324+ return true
325+ case nil:
326+ // No accessor block - this is a stored property, not readonly
327+ return false
328+ }
329+ })
330+ let isReadonly = isLet || isGetterOnly
331+
332+ let exportedProperty = ExportedProperty(
333+ name: propertyName,
334+ type: propertyType,
335+ isReadonly: isReadonly
336+ )
337+
338+ exportedClassByName[className]?.properties.append(exportedProperty)
339+ }
340+
341+ return .skipChildren
342+ }
343+
275344 override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
276345 let name = node.name.text
277346
@@ -284,6 +353,7 @@ public class ExportSwift {
284353 name: name,
285354 constructor: nil,
286355 methods: [],
356+ properties: [],
287357 namespace: namespace
288358 )
289359 exportedClassNames.append(name)
@@ -350,7 +420,8 @@ public class ExportSwift {
350420
351421 class ExportedThunkBuilder {
352422 var body: [CodeBlockItemSyntax] = []
353- var abiParameterForwardings: [LabeledExprSyntax] = []
423+ var liftedParameterExprs: [ExprSyntax] = []
424+ var parameters: [Parameter] = []
354425 var abiParameterSignatures: [(name: String, type: WasmCoreType)] = []
355426 var abiReturnType: WasmCoreType?
356427 let effects: Effects
@@ -369,38 +440,19 @@ public class ExportSwift {
369440 }
370441
371442 func liftParameter(param: Parameter) {
443+ parameters.append(param)
372444 switch param.type {
373445 case .bool:
374- abiParameterForwardings.append(
375- LabeledExprSyntax(
376- label: param.label,
377- expression: ExprSyntax("\(raw: param.name) == 1")
378- )
379- )
446+ liftedParameterExprs.append(ExprSyntax("\(raw: param.name) == 1"))
380447 abiParameterSignatures.append((param.name, .i32))
381448 case .int:
382- abiParameterForwardings.append(
383- LabeledExprSyntax(
384- label: param.label,
385- expression: ExprSyntax("\(raw: param.type.swiftType)(\(raw: param.name))")
386- )
387- )
449+ liftedParameterExprs.append(ExprSyntax("\(raw: param.type.swiftType)(\(raw: param.name))"))
388450 abiParameterSignatures.append((param.name, .i32))
389451 case .float:
390- abiParameterForwardings.append(
391- LabeledExprSyntax(
392- label: param.label,
393- expression: ExprSyntax("\(raw: param.name)")
394- )
395- )
452+ liftedParameterExprs.append(ExprSyntax("\(raw: param.name)"))
396453 abiParameterSignatures.append((param.name, .f32))
397454 case .double:
398- abiParameterForwardings.append(
399- LabeledExprSyntax(
400- label: param.label,
401- expression: ExprSyntax("\(raw: param.name)")
402- )
403- )
455+ liftedParameterExprs.append(ExprSyntax("\(raw: param.name)"))
404456 abiParameterSignatures.append((param.name, .f64))
405457 case .string:
406458 let bytesLabel = "\(param.name)Bytes"
@@ -412,46 +464,40 @@ public class ExportSwift {
412464 }
413465 """
414466 append(prepare)
415- abiParameterForwardings.append(
416- LabeledExprSyntax(
417- label: param.label,
418- expression: ExprSyntax("\(raw: param.name)")
419- )
420- )
467+ liftedParameterExprs.append(ExprSyntax("\(raw: param.name)"))
421468 abiParameterSignatures.append((bytesLabel, .i32))
422469 abiParameterSignatures.append((lengthLabel, .i32))
423470 case .jsObject(nil):
424- abiParameterForwardings.append(
425- LabeledExprSyntax(
426- label: param.label,
427- expression: ExprSyntax("JSObject(id: UInt32(bitPattern: \(raw: param.name)))")
428- )
429- )
471+ liftedParameterExprs.append(ExprSyntax("JSObject(id: UInt32(bitPattern: \(raw: param.name)))"))
430472 abiParameterSignatures.append((param.name, .i32))
431473 case .jsObject(let name):
432- abiParameterForwardings.append(
433- LabeledExprSyntax(
434- label: param.label,
435- expression: ExprSyntax("\(raw: name)(takingThis: UInt32(bitPattern: \(raw: param.name)))")
436- )
474+ liftedParameterExprs.append(
475+ ExprSyntax("\(raw: name)(takingThis: UInt32(bitPattern: \(raw: param.name)))")
437476 )
438477 abiParameterSignatures.append((param.name, .i32))
439478 case .swiftHeapObject:
440479 // UnsafeMutableRawPointer is passed as an i32 pointer
441480 let objectExpr: ExprSyntax =
442481 "Unmanaged<\(raw: param.type.swiftType)>.fromOpaque(\(raw: param.name)).takeUnretainedValue()"
443- abiParameterForwardings.append(
444- LabeledExprSyntax(label: param.label, expression: objectExpr)
445- )
482+ liftedParameterExprs.append(objectExpr)
446483 abiParameterSignatures.append((param.name, .pointer))
447484 case .void:
448485 break
449486 }
450487 }
451488
489+ private func removeFirstLiftedParameter() -> (parameter: Parameter, expr: ExprSyntax) {
490+ let parameter = parameters.removeFirst()
491+ let expr = liftedParameterExprs.removeFirst()
492+ return (parameter, expr)
493+ }
494+
452495 private func renderCallStatement(callee: ExprSyntax, returnType: BridgeType) -> CodeBlockItemSyntax {
496+ let labeledParams = zip(parameters, liftedParameterExprs).map { param, expr in
497+ LabeledExprSyntax(label: param.label, expression: expr)
498+ }
453499 var callExpr: ExprSyntax =
454- "\(raw: callee)(\(raw: abiParameterForwardings .map { $0.description }.joined(separator: ", ")))"
500+ "\(raw: callee)(\(raw: labeledParams .map { $0.description }.joined(separator: ", ")))"
455501 if effects.isAsync {
456502 callExpr = ExprSyntax(
457503 AwaitExprSyntax(awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), expression: callExpr)
@@ -484,14 +530,30 @@ public class ExportSwift {
484530 }
485531
486532 func callMethod(klassName: String, methodName: String, returnType: BridgeType) {
487- let _selfParam = self.abiParameterForwardings.removeFirst ()
533+ let (_, selfExpr) = removeFirstLiftedParameter ()
488534 let item = renderCallStatement(
489- callee: "\(raw: _selfParam ).\(raw: methodName)",
535+ callee: "\(raw: selfExpr ).\(raw: methodName)",
490536 returnType: returnType
491537 )
492538 append(item)
493539 }
494540
541+ func callPropertyGetter(klassName: String, propertyName: String, returnType: BridgeType) {
542+ let (_, selfExpr) = removeFirstLiftedParameter()
543+ let retMutability = returnType == .string ? "var" : "let"
544+ if returnType == .void {
545+ append("\(raw: selfExpr).\(raw: propertyName)")
546+ } else {
547+ append("\(raw: retMutability) ret = \(raw: selfExpr).\(raw: propertyName)")
548+ }
549+ }
550+
551+ func callPropertySetter(klassName: String, propertyName: String) {
552+ let (_, selfExpr) = removeFirstLiftedParameter()
553+ let (_, newValueExpr) = removeFirstLiftedParameter()
554+ append("\(raw: selfExpr).\(raw: propertyName) = \(raw: newValueExpr)")
555+ }
556+
495557 func lowerReturnValue(returnType: BridgeType) {
496558 if effects.isAsync {
497559 // Async functions always return a Promise, which is a JSObject
@@ -717,6 +779,39 @@ public class ExportSwift {
717779 decls.append(builder.render(abiName: method.abiName))
718780 }
719781
782+ // Generate property getters and setters
783+ for property in klass.properties {
784+ // Generate getter
785+ let getterBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
786+ getterBuilder.liftParameter(
787+ param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name))
788+ )
789+ getterBuilder.callPropertyGetter(
790+ klassName: klass.name,
791+ propertyName: property.name,
792+ returnType: property.type
793+ )
794+ getterBuilder.lowerReturnValue(returnType: property.type)
795+ decls.append(getterBuilder.render(abiName: property.getterAbiName(className: klass.name)))
796+
797+ // Generate setter if property is not readonly
798+ if !property.isReadonly {
799+ let setterBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
800+ setterBuilder.liftParameter(
801+ param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name))
802+ )
803+ setterBuilder.liftParameter(
804+ param: Parameter(label: "value", name: "value", type: property.type)
805+ )
806+ setterBuilder.callPropertySetter(
807+ klassName: klass.name,
808+ propertyName: property.name
809+ )
810+ setterBuilder.lowerReturnValue(returnType: .void)
811+ decls.append(setterBuilder.render(abiName: property.setterAbiName(className: klass.name)))
812+ }
813+ }
814+
720815 do {
721816 decls.append(
722817 """
0 commit comments