From 67fad18cf23ebe8c87b548c58fbdb83421998dc1 Mon Sep 17 00:00:00 2001 From: Shivanshu <112751483+shivanshu877@users.noreply.github.com> Date: Sun, 10 May 2026 03:46:02 +0530 Subject: [PATCH 1/4] fix: add isMutating to Effects for mutating struct method support Adds `isMutating: Bool` to the `Effects` struct with backward-compatible Codable implementation. The field is only serialised when true, so existing JSON snapshots are unaffected. Old JSON without the key decodes cleanly via `decodeIfPresent(...) ?? false`. Part of the fix for swiftwasm/JavaScriptKit#736. --- .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 346b7333b..d132888b3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -635,11 +635,35 @@ public struct Effects: Codable, Equatable, Sendable { public var isAsync: Bool public var isThrows: Bool public var isStatic: Bool + public var isMutating: Bool - public init(isAsync: Bool, isThrows: Bool, isStatic: Bool = false) { + public init(isAsync: Bool, isThrows: Bool, isStatic: Bool = false, isMutating: Bool = false) { self.isAsync = isAsync self.isThrows = isThrows self.isStatic = isStatic + self.isMutating = isMutating + } + + private enum CodingKeys: String, CodingKey { + case isAsync, isThrows, isStatic, isMutating + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.isAsync = try container.decode(Bool.self, forKey: .isAsync) + self.isThrows = try container.decode(Bool.self, forKey: .isThrows) + self.isStatic = try container.decode(Bool.self, forKey: .isStatic) + self.isMutating = try container.decodeIfPresent(Bool.self, forKey: .isMutating) ?? false + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(isAsync, forKey: .isAsync) + try container.encode(isThrows, forKey: .isThrows) + try container.encode(isStatic, forKey: .isStatic) + if isMutating { + try container.encode(isMutating, forKey: .isMutating) + } } } From cd676829f9124561398ac4c0bdffc911a2a5ca69 Mon Sep 17 00:00:00 2001 From: Shivanshu <112751483+shivanshu877@users.noreply.github.com> Date: Sun, 10 May 2026 03:51:58 +0530 Subject: [PATCH 2/4] test: add snapshot test input for mutating struct methods --- .../Inputs/MacroSwift/MutatingStructMethod.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/MutatingStructMethod.swift diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/MutatingStructMethod.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/MutatingStructMethod.swift new file mode 100644 index 000000000..23d2e6538 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/MutatingStructMethod.swift @@ -0,0 +1,12 @@ +@JS struct Counter { + var number: Int +} + +extension Counter { + @JS public mutating func increment() { + number += 1 + } + @JS public mutating func add(_ value: Int) { + number += value + } +} From ea9836509893e394ee6b6e19821d6ecaff69ada5 Mon Sep 17 00:00:00 2001 From: Shivanshu <112751483+shivanshu877@users.noreply.github.com> Date: Sun, 10 May 2026 03:57:33 +0530 Subject: [PATCH 3/4] fix: generate mutable self binding for @JS mutating struct methods --- .../Sources/BridgeJSCore/ExportSwift.swift | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index b649b244d..3b1d87de3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -228,14 +228,24 @@ public class ExportSwift { } } - func callMethod(methodName: String, returnType: BridgeType) { - let (_, selfExpr) = removeFirstLiftedParameter() - generateParameterLifting() - let item = renderCallStatement( - callee: "\(raw: selfExpr).\(raw: methodName)", - returnType: returnType - ) - append(item) + func callMethod(methodName: String, returnType: BridgeType, isMutating: Bool = false) { + let (selfParam, selfExpr) = removeFirstLiftedParameter() + if isMutating, case .swiftStruct = selfParam.type { + append("var _self = \(selfExpr)") + generateParameterLifting() + let item = renderCallStatement( + callee: "_self.\(raw: methodName)", + returnType: returnType + ) + append(item) + } else { + generateParameterLifting() + let item = renderCallStatement( + callee: "\(raw: selfExpr).\(raw: methodName)", + returnType: returnType + ) + append(item) + } } /// Generates intermediate variables for stack-using parameters if needed for LIFO compatibility @@ -561,7 +571,7 @@ public class ExportSwift { if method.effects.isStatic { builder.call(name: "\(ownerTypeName).\(method.name)", returnType: method.returnType) } else { - builder.callMethod(methodName: method.name, returnType: method.returnType) + builder.callMethod(methodName: method.name, returnType: method.returnType, isMutating: method.effects.isMutating) } try builder.lowerReturnValue(returnType: method.returnType) return builder.render(abiName: method.abiName) From 360e459b194c4011b285f0d03680ba09e9ba28b5 Mon Sep 17 00:00:00 2001 From: Shivanshu <112751483+shivanshu877@users.noreply.github.com> Date: Sun, 10 May 2026 04:00:04 +0530 Subject: [PATCH 4/4] fix: detect mutating modifier in collectEffects for struct methods --- .../BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index f39ac16f8..2fa051e33 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1198,7 +1198,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { className: classNameForABI ) - guard let effects = collectEffects(signature: node.signature, isStatic: isStatic) else { + let isMutating = node.modifiers.contains { $0.name.tokenKind == .keyword(.mutating) } + guard let effects = collectEffects(signature: node.signature, isStatic: isStatic, isMutating: isMutating) else { return nil } @@ -1213,7 +1214,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { ) } - private func collectEffects(signature: FunctionSignatureSyntax, isStatic: Bool = false) -> Effects? { + private func collectEffects(signature: FunctionSignatureSyntax, isStatic: Bool = false, isMutating: Bool = false) -> Effects? { let isAsync = signature.effectSpecifiers?.asyncSpecifier != nil var isThrows = false if let throwsClause: ThrowsClauseSyntax = signature.effectSpecifiers?.throwsClause { @@ -1234,7 +1235,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { } isThrows = true } - return Effects(isAsync: isAsync, isThrows: isThrows, isStatic: isStatic) + return Effects(isAsync: isAsync, isThrows: isThrows, isStatic: isStatic, isMutating: isMutating) } private func extractNamespace( @@ -1522,7 +1523,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { } } - /// Walks extension members under the matching type’s state, returning whether the type was found. + /// Walks extension members under the matching type's state, returning whether the type was found. /// /// Note: The lookup scans dictionaries keyed by `makeKey(name:namespace:)`, matching only by /// plain name. If two types share a name but differ by namespace, `.first(where:)` picks