From ed4bedb226150c055a4ce0cee994faea41f846c2 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Tue, 20 Oct 2026 10:13:50 +1100 Subject: [PATCH 1/2] BridgeJS: Escape reserved words --- .../Sources/BridgeJSCore/ExportSwift.swift | 18 +- .../Sources/BridgeJSLink/BridgeJSLink.swift | 31 +- .../MacroSwift/ReservedWordParameters.swift | 1 + .../ReservedWordParameters.json | 81 ++++ .../ReservedWordParameters.swift | 13 + .../ReservedWordParameters.d.ts | 18 + .../ReservedWordParameters.js | 382 ++++++++++++++++++ .../BridgeJSRuntimeTests/ExportAPITests.swift | 3 + .../Generated/BridgeJS.swift | 15 + .../Generated/JavaScript/BridgeJS.json | 55 +++ Tests/prelude.mjs | 1 + 11 files changed, 605 insertions(+), 13 deletions(-) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 7d402aa9c..cf0907fa9 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -216,7 +216,7 @@ public class ExportSwift { let liftingInfo = try param.type.liftParameterInfo() let argumentsToLift: [String] if liftingInfo.parameters.count == 1 { - argumentsToLift = [param.name] + argumentsToLift = [param.escapedSwiftIdentifier] } else { argumentsToLift = liftingInfo.parameters.map { (name, _) in param.name + name.capitalizedFirstLetter } } @@ -227,7 +227,9 @@ public class ExportSwift { switch param.type { case .closure(let signature, _): typeNameForIntrinsic = param.type.swiftType - liftingExpr = ExprSyntax("_BJS_Closure_\(raw: signature.mangleName).bridgeJSLift(\(raw: param.name))") + liftingExpr = ExprSyntax( + "_BJS_Closure_\(raw: signature.mangleName).bridgeJSLift(\(raw: param.escapedSwiftIdentifier))" + ) case .swiftStruct(let structName): typeNameForIntrinsic = structName liftingExpr = ExprSyntax("\(raw: structName).bridgeJSLiftParameter()") @@ -252,7 +254,7 @@ public class ExportSwift { ) } - parameterBindings.insert("let \(raw: param.name) = \(liftingExpr)", at: 0) + parameterBindings.insert("let \(raw: param.escapedSwiftIdentifier) = \(liftingExpr)", at: 0) for (name, type) in zip(argumentsToLift, liftingInfo.parameters.map { $0.type }) { abiParameterSignatures.append((name, type)) } @@ -268,14 +270,14 @@ public class ExportSwift { } private func removeFirstLiftedParameter() -> String { - return parameters.removeFirst().name + return parameters.removeFirst().escapedSwiftIdentifier } private func renderCallStatement(callee: ExprSyntax, returnType: BridgeType) -> CodeBlockItemSyntax { let labeledParams = parameters.map { param in LabeledExprSyntax( label: param.label, - expression: DeclReferenceExprSyntax(baseName: .identifier(param.name)) + expression: DeclReferenceExprSyntax(baseName: .identifier(param.escapedSwiftIdentifier)) ) } var callExpr: ExprSyntax = @@ -1593,6 +1595,12 @@ struct ProtocolCodegen { } } +extension Parameter { + fileprivate var escapedSwiftIdentifier: String { + name.backtickIfNeeded() + } +} + extension WasmCoreType { var swiftType: String { switch self { diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 8e7c4939c..ee9531d31 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -1758,7 +1758,7 @@ public struct BridgeJSLink { loweringFragment.parameters.count == 1, "Lowering fragment should have exactly one parameter to lower" ) - let paramName = scope.variable(param.name) + let paramName = scope.variable(param.escapedJSIdentifier) let loweredValues = try loweringFragment.printCode([paramName], context) parameterForwardings.append(contentsOf: loweredValues) } @@ -1950,7 +1950,7 @@ public struct BridgeJSLink { } let parameterSignatures = parameters.map { param in let optional = param.hasDefault ? "?" : "" - return "\(param.name)\(optional): \(resolveTypeScriptType(param.type))" + return "\(param.escapedJSIdentifier)\(optional): \(resolveTypeScriptType(param.type))" } return "(\(parameterSignatures.joined(separator: ", "))): \(returnTypeWithEffect)" } @@ -2669,7 +2669,7 @@ extension BridgeJSLink { if let constructor = klass.constructor { let paramSignatures = constructor.parameters.map { param in let optional = param.hasDefault ? "?" : "" - return "\(param.name)\(optional): \(resolveTypeScriptType(param.type))" + return "\(param.escapedJSIdentifier)\(optional): \(resolveTypeScriptType(param.type))" } printer.write( lines: renderJSDoc(documentation: constructor.documentation, parameters: constructor.parameters) @@ -4321,9 +4321,9 @@ enum DefaultValueUtils { return parameters.map { param in if let defaultValue = param.defaultValue { let defaultJs = format(defaultValue, as: .javascript, resolveTypeName: resolveTypeName) - return "\(param.name) = \(defaultJs)" + return "\(param.escapedJSIdentifier) = \(defaultJs)" } - return param.name + return param.escapedJSIdentifier }.joined(separator: ", ") } } @@ -4348,11 +4348,11 @@ extension BridgeJSLink { } switch (docText, defaultValue) { case let (.some(text), .some(value)): - tagLines.append("@param \(parameter.name) \(text) (default: \(value))") + tagLines.append("@param \(parameter.escapedJSIdentifier) \(text) (default: \(value))") case let (.some(text), .none): - tagLines.append("@param \(parameter.name) \(text)") + tagLines.append("@param \(parameter.escapedJSIdentifier) \(text)") case let (.none, .some(value)): - tagLines.append("@param \(parameter.name) - Optional parameter (default: \(value))") + tagLines.append("@param \(parameter.escapedJSIdentifier) - Optional parameter (default: \(value))") case (.none, .none): continue } @@ -4477,6 +4477,21 @@ struct BridgeJSLinkError: Error { let message: String } +extension Parameter { + // From: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words + private static let javaScriptReservedWords: Set = [ + "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", + "export", "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "new", + "null", "return", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", + "with", "let", "static", "yield", "await", "enum", "implements", "interface", "package", "private", + "protected", "public", "arguments", "eval", + ] + + fileprivate var escapedJSIdentifier: String { + Self.javaScriptReservedWords.contains(name) ? "_\(name)" : name + } +} + extension ClosureSignature { fileprivate func renderTSFunctionType(renderType: (BridgeType) -> String) -> String { let renderedParameters = parameters.enumerated().map { index, parameter in diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift new file mode 100644 index 000000000..2c4c062cb --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift @@ -0,0 +1 @@ +@JS func check(extension: Int, break: Double, function: Bool, in: String) {} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json new file mode 100644 index 000000000..c27a4eb46 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json @@ -0,0 +1,81 @@ +{ + "exported" : { + "aliases" : [ + + ], + "classes" : [ + + ], + "enums" : [ + + ], + "exposeToGlobal" : false, + "functions" : [ + { + "abiName" : "bjs_check", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "check", + "parameters" : [ + { + "label" : "extension", + "name" : "extension", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "break", + "name" : "break", + "type" : { + "double" : { + + } + } + }, + { + "label" : "function", + "name" : "function", + "type" : { + "bool" : { + + } + } + }, + { + "label" : "in", + "name" : "in", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "protocols" : [ + + ], + "structs" : [ + + ] + }, + "moduleName" : "TestModule", + "usedExternalModules" : [ + + ] +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift new file mode 100644 index 000000000..b09e1a76d --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift @@ -0,0 +1,13 @@ +@_expose(wasm, "bjs_check") +@_cdecl("bjs_check") +public func _bjs_check(_ `extension`: Int32, _ `break`: Float64, _ function: Int32, _ inBytes: Int32, _ inLength: Int32) -> Void { + #if arch(wasm32) + let `in` = String.bridgeJSLiftParameter(inBytes, inLength) + let function = Bool.bridgeJSLiftParameter(function) + let `break` = Double.bridgeJSLiftParameter(`break`) + let `extension` = Int.bridgeJSLiftParameter(`extension`) + check(extension: `extension`, break: `break`, function: function, in: `in`) + #else + fatalError("Only available on WebAssembly") + #endif +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts new file mode 100644 index 000000000..027a6b9db --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts @@ -0,0 +1,18 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export type Exports = { + check(extension: number, _break: number, _function: boolean, _in: string): void; +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js new file mode 100644 index 000000000..0abe5fdbf --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js @@ -0,0 +1,382 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + let decodeString; + let decodeUTF8; + const immortalStrings = new Map(); + const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let strStack = []; + let i32Stack = []; + let i64Stack = []; + let f32Stack = []; + let f64Stack = []; + let ptrStack = []; + let taStack = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + bjs["swift_js_return_string"] = function(word0, word1, word2) { + tmpRetString = decodeString(word0, word1, word2); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + swift.memory.release(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr >>> 0); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(word0, word1, word2) { + return swift.memory.retain(decodeString(word0, word1, word2)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_i32"] = function(v) { + i32Stack.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + f32Stack.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + f64Stack.push(v); + } + bjs["swift_js_push_string"] = function(word0, word1, word2) { + const value = decodeString(word0, word1, word2); + strStack.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return i32Stack.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return f32Stack.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return f64Stack.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + ptrStack.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return ptrStack.pop(); + } + bjs["swift_js_push_i64"] = function(v) { + i64Stack.push(v); + } + bjs["swift_js_pop_i64"] = function() { + return i64Stack.pop(); + } + const taCtors = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]; + bjs["swift_js_push_typed_array"] = function(kind, ptr, count) { + const Ctor = taCtors[kind]; + const byteLen = count * Ctor.BYTES_PER_ELEMENT; + const copy = memory.buffer.slice(ptr, ptr + byteLen); + taStack.push(Array.from(new Ctor(copy))); + } + bjs["bjs_core_register_type_handles"] = function() {}; + const __bjs_promiseSettlers = Symbol("JavaScriptKit.promiseSettlers"); + bjs["swift_js_make_promise"] = function() { + let resolve, reject; + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + promise[__bjs_promiseSettlers] = { resolve, reject }; + return swift.memory.retain(promise); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, word0, word1, word2) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = decodeString(word0, word1, word2); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + bjs["swift_js_closure_unregister"] = function(funcRef) {} + }, + setInstance: (i) => { + if (memory !== i.exports.memory) immortalStrings.clear(); + instance = i; + memory = instance.exports.memory; + + decodeUTF8 = (ptr, len) => { const bytes = new Uint8Array(memory.buffer, ptr >>> 0, len >>> 0); return textDecoder.decode(bytes); } + decodeString = (() => { + const byteAt = (word0, word1, byteIndex) => + byteIndex < 4 + ? (word0 >>> (byteIndex * 8)) & 255 + : (word1 >>> ((byteIndex - 4) * 8)) & 255; + // Valid small Swift strings need at most eight UTF-16 code units. + const decodeSmallUTF8 = (word0, word1, count) => { + if (count > 8) { + throw new Error(`Unsupported Swift inline String length: ${count} UTF-8 bytes (maximum 8). The Swift String layout may have changed; update JavaScriptKit and regenerate BridgeJS glue.`); + } + let byteIndex = 0, + unitCount = 0, + u0 = 0, + u1 = 0, + u2 = 0, + u3 = 0, + u4 = 0, + u5 = 0, + u6 = 0, + u7 = 0; + while (byteIndex < count) { + const b0 = byteAt(word0, word1, byteIndex++); + let codePoint; + if (b0 < 128) codePoint = b0; + else if (b0 < 224) codePoint = ((b0 & 31) << 6) | (byteAt(word0, word1, byteIndex++) & 63); + else if (b0 < 240) { + const b1 = byteAt(word0, word1, byteIndex++), + b2 = byteAt(word0, word1, byteIndex++); + codePoint = ((b0 & 15) << 12) | ((b1 & 63) << 6) | (b2 & 63); + } else { + const b1 = byteAt(word0, word1, byteIndex++), + b2 = byteAt(word0, word1, byteIndex++), + b3 = byteAt(word0, word1, byteIndex++); + codePoint = ((b0 & 7) << 18) | ((b1 & 63) << 12) | ((b2 & 63) << 6) | (b3 & 63); + } + const supplementary = codePoint > 0xffff; + const codeUnit = supplementary + ? 0xd800 + ((codePoint - 0x10000) >>> 10) + : codePoint; + switch (unitCount++) { + case 0: + u0 = codeUnit; + break; + case 1: + u1 = codeUnit; + break; + case 2: + u2 = codeUnit; + break; + case 3: + u3 = codeUnit; + break; + case 4: + u4 = codeUnit; + break; + case 5: + u5 = codeUnit; + break; + case 6: + u6 = codeUnit; + break; + case 7: + u7 = codeUnit; + break; + } + if (supplementary) { + const lowSurrogate = 0xdc00 + ((codePoint - 0x10000) & 1023); + switch (unitCount++) { + case 0: + u0 = lowSurrogate; + break; + case 1: + u1 = lowSurrogate; + break; + case 2: + u2 = lowSurrogate; + break; + case 3: + u3 = lowSurrogate; + break; + case 4: + u4 = lowSurrogate; + break; + case 5: + u5 = lowSurrogate; + break; + case 6: + u6 = lowSurrogate; + break; + case 7: + u7 = lowSurrogate; + break; + } + } + } + switch (unitCount) { + case 0: + return ""; + case 1: + return String.fromCharCode(u0); + case 2: + return String.fromCharCode(u0, u1); + case 3: + return String.fromCharCode(u0, u1, u2); + case 4: + return String.fromCharCode(u0, u1, u2, u3); + case 5: + return String.fromCharCode(u0, u1, u2, u3, u4); + case 6: + return String.fromCharCode(u0, u1, u2, u3, u4, u5); + case 7: + return String.fromCharCode(u0, u1, u2, u3, u4, u5, u6); + case 8: + return String.fromCharCode(u0, u1, u2, u3, u4, u5, u6, u7); + } + }; + const decodeSmall = (word0, word1, word2) => { + const count = (word2 >>> 8) & 0x0f; + if ((word2 & 0x4000 /* ASCII */) === 0) return decodeSmallUTF8(word0, word1, count); + const char = String.fromCharCode; + switch (count) { + case 0: return ""; + case 1: return char(word0 & 0xff); + case 2: return char(word0 & 0xff, (word0 >>> 8) & 0xff); + case 3: return char(word0 & 0xff, (word0 >>> 8) & 0xff, (word0 >>> 16) & 0xff); + case 4: return char(word0 & 0xff, (word0 >>> 8) & 0xff, (word0 >>> 16) & 0xff, word0 >>> 24); + case 5: return char(word0 & 0xff, (word0 >>> 8) & 0xff, (word0 >>> 16) & 0xff, word0 >>> 24, word1 & 0xff); + case 6: return char(word0 & 0xff, (word0 >>> 8) & 0xff, (word0 >>> 16) & 0xff, word0 >>> 24, word1 & 0xff, (word1 >>> 8) & 0xff); + case 7: return char(word0 & 0xff, (word0 >>> 8) & 0xff, (word0 >>> 16) & 0xff, word0 >>> 24, word1 & 0xff, (word1 >>> 8) & 0xff, (word1 >>> 16) & 0xff); + case 8: return char(word0 & 0xff, (word0 >>> 8) & 0xff, (word0 >>> 16) & 0xff, word0 >>> 24, word1 & 0xff, (word1 >>> 8) & 0xff, (word1 >>> 16) & 0xff, word1 >>> 24); + default: + throw new Error(`Unsupported Swift inline String length: ${count} UTF-8 bytes (maximum 8). The Swift String layout may have changed; update JavaScriptKit and regenerate BridgeJS glue.`); + } + }; + return (word0, word1, word2) => { + if (word2 & 0x2000 /* small */) return decodeSmall(word0, word1, word2); + const ptr = (word1 + 20 /* nativeBias */) >>> 0; + const len = word0 >>> 0; + if ((word2 & 0x8000 /* immortal */) === 0 || len >= 0x200000 /* 2^21 Number key */) return decodeUTF8(ptr, len); + const key = ptr + len * 0x100000000; + const cached = immortalStrings.get(key); + if (cached !== undefined) return cached; + const value = decodeUTF8(ptr, len); + immortalStrings.set(key, value); + return value; + }; + })(); + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const exports = { + check: function bjs_check(extension, _break, _function, _in) { + const _inBytes = textEncoder.encode(_in); + const _inId = swift.memory.retain(_inBytes); + instance.exports.bjs_check(extension, _break, _function, _inId, _inBytes.length); + }, + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index c231d0ce9..9e5892a87 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -22,6 +22,9 @@ func runJsWorks() -> Void @JS func roundTripString(v: String) -> String { return v } +@JS func describeReservedWordParameters(extension: Int, break: Double, function: Bool, in: String) -> String { + return "\(`extension`):\(`break`):\(function):\(`in`)" +} @JS func roundTripSwiftHeapObject(v: Greeter) -> Greeter { return v } diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index b89bdb510..884ecb8e1 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -10324,6 +10324,21 @@ public func _bjs_roundTripString(_ vBytes: Int32, _ vLength: Int32) -> Void { #endif } +@_expose(wasm, "bjs_describeReservedWordParameters") +@_cdecl("bjs_describeReservedWordParameters") +public func _bjs_describeReservedWordParameters(_ `extension`: Int32, _ `break`: Float64, _ function: Int32, _ inBytes: Int32, _ inLength: Int32) -> Void { + #if arch(wasm32) + let `in` = String.bridgeJSLiftParameter(inBytes, inLength) + let function = Bool.bridgeJSLiftParameter(function) + let `break` = Double.bridgeJSLiftParameter(`break`) + let `extension` = Int.bridgeJSLiftParameter(`extension`) + let ret = describeReservedWordParameters(extension: `extension`, break: `break`, function: function, in: `in`) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_roundTripSwiftHeapObject") @_cdecl("bjs_roundTripSwiftHeapObject") public func _bjs_roundTripSwiftHeapObject(_ v: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 9d5da0a7f..8598361b6 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -15214,6 +15214,61 @@ } } }, + { + "abiName" : "bjs_describeReservedWordParameters", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "describeReservedWordParameters", + "parameters" : [ + { + "label" : "extension", + "name" : "extension", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "break", + "name" : "break", + "type" : { + "double" : { + + } + } + }, + { + "label" : "function", + "name" : "function", + "type" : { + "bool" : { + + } + } + }, + { + "label" : "in", + "name" : "in", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, { "abiName" : "bjs_roundTripSwiftHeapObject", "effects" : { diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index f54e3a149..15631bcbe 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -305,6 +305,7 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { ]) { assert.equal(exports.roundTripString(v), v); } + assert.equal(exports.describeReservedWordParameters(1, 2.5, true, "in"), "1:2.5:true:in"); const dict = { a: 1, b: 2 }; assert.deepEqual(exports.roundTripDictionaryExport(dict), dict); const optDict = { hello: "world" }; From 89057a154b644b2ef12599636b44f46548dd279c Mon Sep 17 00:00:00 2001 From: William Taylor Date: Wed, 28 Oct 2026 16:12:22 +1100 Subject: [PATCH 2/2] BridgeJS: Escape on import too --- .../Sources/BridgeJSCore/ExportSwift.swift | 13 +- .../Sources/BridgeJSCore/ImportTS.swift | 42 +- .../BridgeJSCore/SwiftToSkeleton.swift | 2 +- .../Sources/BridgeJSLink/BridgeJSLink.swift | 122 +++--- .../Sources/BridgeJSLink/JSGlueGen.swift | 28 +- .../BridgeJSMacros/JSMacroSupport.swift | 5 +- .../MacroSwift/ReservedWordParameters.swift | 22 + .../ReservedWordParameters.json | 378 ++++++++++++++++++ .../ReservedWordParameters.swift | 267 +++++++++++++ .../ReservedWordParameters.d.ts | 27 ++ .../ReservedWordParameters.js | 172 ++++++++ .../BridgeJSRuntimeTests/ExportAPITests.swift | 12 + .../Generated/BridgeJS.swift | 98 +++++ .../Generated/JavaScript/BridgeJS.json | 199 +++++++++ .../BridgeJSRuntimeTests/ImportAPITests.swift | 10 + Tests/prelude.mjs | 7 + 16 files changed, 1296 insertions(+), 108 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index cf0907fa9..3197f559c 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -213,10 +213,11 @@ public class ExportSwift { func liftParameter(param: Parameter) throws { parameters.append(param) + let escapedName = param.escapedSwiftIdentifier let liftingInfo = try param.type.liftParameterInfo() let argumentsToLift: [String] if liftingInfo.parameters.count == 1 { - argumentsToLift = [param.escapedSwiftIdentifier] + argumentsToLift = [escapedName] } else { argumentsToLift = liftingInfo.parameters.map { (name, _) in param.name + name.capitalizedFirstLetter } } @@ -228,7 +229,7 @@ public class ExportSwift { case .closure(let signature, _): typeNameForIntrinsic = param.type.swiftType liftingExpr = ExprSyntax( - "_BJS_Closure_\(raw: signature.mangleName).bridgeJSLift(\(raw: param.escapedSwiftIdentifier))" + "_BJS_Closure_\(raw: signature.mangleName).bridgeJSLift(\(raw: escapedName))" ) case .swiftStruct(let structName): typeNameForIntrinsic = structName @@ -254,7 +255,7 @@ public class ExportSwift { ) } - parameterBindings.insert("let \(raw: param.escapedSwiftIdentifier) = \(liftingExpr)", at: 0) + parameterBindings.insert("let \(raw: param.escapedSwiftBindingIdentifier) = \(liftingExpr)", at: 0) for (name, type) in zip(argumentsToLift, liftingInfo.parameters.map { $0.type }) { abiParameterSignatures.append((name, type)) } @@ -1595,12 +1596,6 @@ struct ProtocolCodegen { } } -extension Parameter { - fileprivate var escapedSwiftIdentifier: String { - name.backtickIfNeeded() - } -} - extension WasmCoreType { var swiftType: String { switch self { diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index 841c0409c..504b955d2 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -143,7 +143,8 @@ public struct ImportTS { } func lowerParameter(param: Parameter) throws { - if let genericPush = param.type.genericStackPushStatement(value: param.name) { + let escapedName = param.escapedSwiftIdentifier + if let genericPush = param.type.genericStackPushStatement(value: escapedName) { stackLoweringStmts.insert(genericPush, at: 0) return } @@ -153,10 +154,10 @@ public struct ImportTS { switch param.type { case .closure(let signature, useJSTypedClosure: false): let jsTypedClosureType = BridgeType.closure(signature, useJSTypedClosure: true).swiftType - body.write("let \(param.name) = \(jsTypedClosureType)(\(param.name))") + body.write("let \(param.escapedSwiftBindingIdentifier) = \(jsTypedClosureType)(\(escapedName))") // The just created JSObject is not owned by the caller unlike those passed in parameters, // so we need to extend its lifetime during the call to ensure the JSObject.id is valid. - valuesToExtendLifetimeDuringCall.append(param.name) + valuesToExtendLifetimeDuringCall.append(escapedName) default: break } @@ -177,7 +178,7 @@ public struct ImportTS { if loweringInfo.useBorrowing { let returnVariableName = "ret\(borrowedArguments.count)" let assign = needsReturnVariable ? "let \(returnVariableName) = " : "" - body.write("\(assign)\(param.name).bridgeJSWithLoweredParameter { \(pattern) in") + body.write("\(assign)\(escapedName).bridgeJSWithLoweredParameter { \(pattern) in") body.indent() borrowedArguments.append( BorrowedArgument( @@ -190,10 +191,10 @@ public struct ImportTS { ) } else if case .nullable(.swiftProtocol(let protocolName), _) = param.type, context == .exportSwift { body.write("let \(pattern): (Int32, Int32)") - body.write("if let \(param.name) {") + body.write("if let \(param.escapedSwiftBindingIdentifier) {") body.indent { body.write( - "\(pattern) = (1, _bridgeJSUnwrapProtocolExportable(\(param.name), \"\(protocolName)\").bridgeJSLowerAsProtocolReturn())" + "\(pattern) = (1, _bridgeJSUnwrapProtocolExportable(\(escapedName), \"\(protocolName)\").bridgeJSLowerAsProtocolReturn())" ) } body.write("} else {") @@ -205,10 +206,10 @@ public struct ImportTS { let initializerExpr: ExprSyntax if case .swiftProtocol(let protocolName) = param.type, context == .exportSwift { initializerExpr = ExprSyntax( - "_bridgeJSUnwrapProtocolExportable(\(raw: param.name), \"\(raw: protocolName)\").bridgeJSLowerAsProtocolReturn()" + "_bridgeJSUnwrapProtocolExportable(\(raw: escapedName), \"\(raw: protocolName)\").bridgeJSLowerAsProtocolReturn()" ) } else { - initializerExpr = ExprSyntax("\(raw: param.name).bridgeJSLowerParameter()") + initializerExpr = ExprSyntax("\(raw: escapedName).bridgeJSLowerParameter()") } let binding = loweringInfo.loweredParameters.isEmpty ? "_" : pattern @@ -218,10 +219,10 @@ public struct ImportTS { // Add to signatures and forwardings (unified for both single and multiple) for (index, (paramName, type)) in loweringInfo.loweredParameters.enumerated() { - // For single parameter, use param.name; for multiple, use constructed name + // For single parameter, use the escaped name; for multiple, use constructed name let abiParamName: String if loweringInfo.loweredParameters.count == 1 { - abiParamName = param.name + abiParamName = escapedName } else { abiParamName = "\(param.name)\(paramName.capitalizedFirstLetter)" } @@ -723,20 +724,21 @@ struct SwiftSignatureBuilder { var out = "(" out += parameters.map { param in let label = param.label ?? param.name + let escapedName = param.escapedSwiftIdentifier let paramType = buildParameterTypeSyntax(from: param.type) if useWildcardLabels { // Always use wildcard labels: "_ name: Type" - return "_ \(param.name): \(paramType)" - } else if label == param.name { + return "_ \(escapedName): \(paramType)" + } else if SwiftToSkeleton.normalizeIdentifier(label) == param.name { // External label same as parameter name: "count: Int" - return "\(param.name): \(paramType)" + return "\(escapedName): \(paramType)" } else if param.label == nil { // No label specified: use wildcard "_ name: Type" - return "_ \(param.name): \(paramType)" + return "_ \(escapedName): \(paramType)" } else { // External label differs: "label count: Int" - return "\(label) \(param.name): \(paramType)" + return "\(label) \(escapedName): \(paramType)" } }.joined(separator: ", ") out += ")" @@ -1065,3 +1067,13 @@ extension String { return self.isValidSwiftIdentifier(for: .variableName) ? self : "`\(self)`" } } + +extension Parameter { + var escapedSwiftIdentifier: String { + name == "self" ? name : name.backtickIfNeeded() + } + + var escapedSwiftBindingIdentifier: String { + name.backtickIfNeeded() + } +} diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index ad3722dac..ef39000b1 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1587,7 +1587,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { continue } - let name = param.secondName?.text ?? param.firstName.text + let name = SwiftToSkeleton.normalizeIdentifier(param.secondName?.text ?? param.firstName.text) let label = param.firstName.text let defaultValue: DefaultValue? diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index ee9531d31..686f8d1e0 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -1730,6 +1730,9 @@ public struct BridgeJSLink { class ExportedThunkBuilder { var body: CodeFragmentPrinter var parameterForwardings: [String] = [] + + private var signatureParameters: [(parameter: Parameter, name: String)] = [] + let effects: Effects let scope: JSGlueVariableScope let context: IntrinsicJSFragment.PrintCodeContext @@ -1759,6 +1762,7 @@ public struct BridgeJSLink { "Lowering fragment should have exactly one parameter to lower" ) let paramName = scope.variable(param.escapedJSIdentifier) + signatureParameters.append((param, paramName)) let loweredValues = try loweringFragment.printCode([paramName], context) parameterForwardings.append(contentsOf: loweredValues) } @@ -1767,6 +1771,20 @@ public struct BridgeJSLink { parameterForwardings.append("this.pointer") } + var parameterList: String { + signatureParameters.map { parameter, name in + guard let defaultValue = parameter.defaultValue else { + return name + } + let defaultJs = DefaultValueUtils.format( + defaultValue, + as: .javascript, + resolveTypeName: context.defaultValueTypeName + ) + return "\(name) = \(defaultJs)" + }.joined(separator: ", ") + } + func call(abiName: String, returnType: BridgeType) throws -> String? { if effects.isAsync { return try _call(abiName: abiName, returnType: .jsObject(nil)) @@ -1830,17 +1848,11 @@ public struct BridgeJSLink { func renderFunction( name: String, - parameters: [Parameter], returnExpr: String?, declarationPrefixKeyword: String? ) -> [String] { let printer = CodeFragmentPrinter() - let parameterList = DefaultValueUtils.formatParameterList( - parameters, - resolveTypeName: context.defaultValueTypeName - ) - printer.write( "\(declarationPrefixKeyword.map { "\($0) "} ?? "")\(name)(\(parameterList)) {" ) @@ -1948,11 +1960,15 @@ public struct BridgeJSLink { } else { returnTypeWithEffect = resolveTypeScriptType(returnType) } - let parameterSignatures = parameters.map { param in + return "\(renderTSParameterList(parameters)): \(returnTypeWithEffect)" + } + + private func renderTSParameterList(_ parameters: [Parameter]) -> String { + let parameterSignatures = zip(parameters, parameters.uniqueJSIdentifiers).map { param, name in let optional = param.hasDefault ? "?" : "" - return "\(param.escapedJSIdentifier)\(optional): \(resolveTypeScriptType(param.type))" + return "\(name)\(optional): \(resolveTypeScriptType(param.type))" } - return "(\(parameterSignatures.joined(separator: ", "))): \(returnTypeWithEffect)" + return "(\(parameterSignatures.joined(separator: ", ")))" } private func renderGenericClause(_ genericParameters: [GenericParameter]) -> String { @@ -2062,11 +2078,7 @@ public struct BridgeJSLink { ) let constructorPrinter = CodeFragmentPrinter() - let paramList = DefaultValueUtils.formatParameterList( - constructor.parameters, - resolveTypeName: thunkBuilder.context.defaultValueTypeName - ) - constructorPrinter.write("init: function(\(paramList)) {") + constructorPrinter.write("init: function(\(thunkBuilder.parameterList)) {") constructorPrinter.indent { thunkBuilder.renderFunctionBody(into: constructorPrinter, returnExpr: returnExpr) } @@ -2290,7 +2302,6 @@ extension BridgeJSLink { let returnExpr = try thunkBuilder.call(abiName: function.abiName, returnType: function.returnType) let funcLines = thunkBuilder.renderFunction( name: function.abiName, - parameters: function.parameters, returnExpr: returnExpr, declarationPrefixKeyword: "function" ) @@ -2341,7 +2352,6 @@ extension BridgeJSLink { let funcLines = thunkBuilder.renderFunction( name: function.resolvedJSName, - parameters: function.parameters, returnExpr: returnExpr, declarationPrefixKeyword: "static" ) @@ -2371,11 +2381,7 @@ extension BridgeJSLink { let returnExpr = try thunkBuilder.call(abiName: function.abiName, returnType: function.returnType) let printer = CodeFragmentPrinter() - let parameterList = DefaultValueUtils.formatParameterList( - function.parameters, - resolveTypeName: thunkBuilder.context.defaultValueTypeName - ) - printer.write("\(function.resolvedJSName)(\(parameterList)) {") + printer.write("\(function.resolvedJSName)(\(thunkBuilder.parameterList)) {") printer.indent { thunkBuilder.renderFunctionBody(into: printer, returnExpr: returnExpr) } @@ -2407,7 +2413,6 @@ extension BridgeJSLink { let funcLines = thunkBuilder.renderFunction( name: function.abiName, - parameters: function.parameters, returnExpr: returnExpr, declarationPrefixKeyword: "function" ) @@ -2431,12 +2436,8 @@ extension BridgeJSLink { let returnExpr = try thunkBuilder.call(abiName: method.abiName, returnType: method.returnType) let methodPrinter = CodeFragmentPrinter() - let parameterList = DefaultValueUtils.formatParameterList( - method.parameters, - resolveTypeName: thunkBuilder.context.defaultValueTypeName - ) methodPrinter.write( - "\(method.resolvedJSName): function(\(parameterList)) {" + "\(method.resolvedJSName): function(\(thunkBuilder.parameterList)) {" ) methodPrinter.indent { thunkBuilder.renderFunctionBody(into: methodPrinter, returnExpr: returnExpr) @@ -2486,7 +2487,7 @@ extension BridgeJSLink { returnType: .void ) - propertyPrinter.write("set \(property.resolvedJSName)(value) {") + propertyPrinter.write("set \(property.resolvedJSName)(\(setterThunkBuilder.parameterList)) {") propertyPrinter.indent { setterThunkBuilder.renderFunctionBody(into: propertyPrinter, returnExpr: nil) } @@ -2540,13 +2541,8 @@ extension BridgeJSLink { try thunkBuilder.lowerParameter(param: param) } - let constructorParamList = DefaultValueUtils.formatParameterList( - constructor.parameters, - resolveTypeName: thunkBuilder.context.defaultValueTypeName - ) - jsPrinter.indent { - jsPrinter.write("constructor(\(constructorParamList)) {") + jsPrinter.write("constructor(\(thunkBuilder.parameterList)) {") let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) let constructCall = "\(klass.resolvedJSName).__construct(\(returnExpr))" jsPrinter.indent { @@ -2574,7 +2570,6 @@ extension BridgeJSLink { jsPrinter.write( lines: thunkBuilder.renderFunction( name: method.resolvedJSName, - parameters: method.parameters, returnExpr: returnExpr, declarationPrefixKeyword: "static" ) @@ -2595,7 +2590,6 @@ extension BridgeJSLink { jsPrinter.write( lines: thunkBuilder.renderFunction( name: method.resolvedJSName, - parameters: method.parameters, returnExpr: returnExpr, declarationPrefixKeyword: nil ) @@ -2667,14 +2661,10 @@ extension BridgeJSLink { printer.write("class \(klass.resolvedJSName) {") printer.indent { if let constructor = klass.constructor { - let paramSignatures = constructor.parameters.map { param in - let optional = param.hasDefault ? "?" : "" - return "\(param.escapedJSIdentifier)\(optional): \(resolveTypeScriptType(param.type))" - } printer.write( lines: renderJSDoc(documentation: constructor.documentation, parameters: constructor.parameters) ) - printer.write("constructor(\(paramSignatures.joined(separator: ", ")));") + printer.write("constructor\(renderTSParameterList(constructor.parameters));") } for method in klass.methods.sorted(by: { $0.resolvedJSName < $1.resolvedJSName }) { let staticKeyword = method.effects.isStatic ? "static " : "" @@ -2720,7 +2710,6 @@ extension BridgeJSLink { jsPrinter.write( lines: getterThunkBuilder.renderFunction( name: property.resolvedJSName, - parameters: [], returnExpr: getterReturnExpr, declarationPrefixKeyword: getterKeyword ) @@ -2748,7 +2737,6 @@ extension BridgeJSLink { jsPrinter.write( lines: setterThunkBuilder.renderFunction( name: property.resolvedJSName, - parameters: [.init(label: nil, name: "value", type: property.type)], returnExpr: nil, declarationPrefixKeyword: setterKeyword ) @@ -2823,7 +2811,7 @@ extension BridgeJSLink { message: "Generic codec for '\(name)' was not declared before lifting parameter '\(param.name)'" ) } - let valueVar = scope.variable(param.name) + let valueVar = scope.variable(param.escapedJSIdentifier) let liftExpr = GenericJSCodegen.genericCodecLiftExpression(type: param.type, codec: codecVar) ?? "\(codecVar).lift()" @@ -2836,8 +2824,9 @@ extension BridgeJSLink { if liftingFragment.parameters.count == 0 { valuesToLift = [] } else if liftingFragment.parameters.count == 1 { - parameterNames.append(param.name) - valuesToLift = [scope.variable(param.name)] + let name = scope.variable(param.escapedJSIdentifier) + parameterNames.append(name) + valuesToLift = [name] } else { valuesToLift = liftingFragment.parameters.map { scope.variable(param.name + $0.capitalizedFirstLetter) } parameterNames.append(contentsOf: valuesToLift) @@ -3525,7 +3514,7 @@ extension BridgeJSLink { ) let setterPrinter = CodeFragmentPrinter() - setterPrinter.write("set \(property.resolvedJSName)(value) {") + setterPrinter.write("set \(property.resolvedJSName)(\(setterThunkBuilder.parameterList)) {") setterPrinter.indent { setterPrinter.write(contentsOf: setterThunkBuilder.body) setterPrinter.write(lines: setterThunkBuilder.checkExceptionLines()) @@ -4312,20 +4301,6 @@ enum DefaultValueUtils { .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") } - - /// Generates a JavaScript parameter list with default values - static func formatParameterList( - _ parameters: [Parameter], - resolveTypeName: (BridgeType, OutputFormat) -> String? = { _, _ in nil } - ) -> String { - return parameters.map { param in - if let defaultValue = param.defaultValue { - let defaultJs = format(defaultValue, as: .javascript, resolveTypeName: resolveTypeName) - return "\(param.escapedJSIdentifier) = \(defaultJs)" - } - return param.escapedJSIdentifier - }.joined(separator: ", ") - } } extension BridgeJSLink { @@ -4337,7 +4312,7 @@ extension BridgeJSLink { let resolveTypeName = makeCodecPrintContext(printer: CodeFragmentPrinter()).defaultValueTypeName var tagLines: [String] = [] - for parameter in parameters { + for (parameter, name) in zip(parameters, parameters.uniqueJSIdentifiers) { let docText = parsed.parameter(named: parameter.name) let defaultValue = parameter.defaultValue.map { DefaultValueUtils.format( @@ -4348,11 +4323,11 @@ extension BridgeJSLink { } switch (docText, defaultValue) { case let (.some(text), .some(value)): - tagLines.append("@param \(parameter.escapedJSIdentifier) \(text) (default: \(value))") + tagLines.append("@param \(name) \(text) (default: \(value))") case let (.some(text), .none): - tagLines.append("@param \(parameter.escapedJSIdentifier) \(text)") + tagLines.append("@param \(name) \(text)") case let (.none, .some(value)): - tagLines.append("@param \(parameter.escapedJSIdentifier) - Optional parameter (default: \(value))") + tagLines.append("@param \(name) - Optional parameter (default: \(value))") case (.none, .none): continue } @@ -4492,6 +4467,23 @@ extension Parameter { } } +extension [Parameter] { + fileprivate var uniqueJSIdentifiers: [String] { + var names: [String] = [] + for parameter in self { + let escapedName = parameter.escapedJSIdentifier + var name = escapedName + var suffix = 1 + while names.contains(name) { + name = escapedName + suffix.description + suffix += 1 + } + names.append(name) + } + return names + } +} + extension ClosureSignature { fileprivate func renderTSFunctionType(renderType: (BridgeType) -> String) -> String { let renderedParameters = parameters.enumerated().map { index, parameter in diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index 2da07f2be..673a64212 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -2586,25 +2586,21 @@ struct IntrinsicJSFragment: Sendable { // Attach instance methods to the struct instance for method in structDef.methods where !method.effects.isStatic { - let paramList = DefaultValueUtils.formatParameterList( - method.parameters, - resolveTypeName: context.defaultValueTypeName + let builder = BridgeJSLink.ExportedThunkBuilder( + effects: method.effects, + context: context + ) + builder.body.write( + "\(JSGlueVariableScope.reservedStructHelpers).\(context.scope.helperKey(forTypeNamed: structDef.swiftCallName)).lower(this);" ) + for param in method.parameters { + try builder.lowerParameter(param: param) + } + let returnExpr = try builder.call(abiName: method.abiName, returnType: method.returnType) printer.write( - "\(instanceVar).\(method.resolvedJSName) = function(\(paramList)) {" + "\(instanceVar).\(method.resolvedJSName) = function(\(builder.parameterList)) {" ) - try printer.indent { - let builder = BridgeJSLink.ExportedThunkBuilder( - effects: method.effects, - context: context - ) - builder.body.write( - "\(JSGlueVariableScope.reservedStructHelpers).\(context.scope.helperKey(forTypeNamed: structDef.swiftCallName)).lower(this);" - ) - for param in method.parameters { - try builder.lowerParameter(param: param) - } - let returnExpr = try builder.call(abiName: method.abiName, returnType: method.returnType) + printer.indent { builder.renderFunctionBody(into: printer, returnExpr: returnExpr) } printer.write("}.bind(\(instanceVar));") diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift index 224462ddb..c347bfb21 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift @@ -90,8 +90,9 @@ enum JSMacroHelper { static func parameterNames(_ parameters: FunctionParameterListSyntax) -> [String] { parameters.compactMap { param in let nameToken = param.secondName ?? param.firstName - let nameText = nameToken.text - return nameText == "_" ? nil : nameText + guard nameToken.text != "_" else { return nil } + let nameText = stripBackticks(nameToken.text) + return nameText.isValidSwiftIdentifier(for: .variableName) ? nameText : "`\(nameText)`" } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift index 2c4c062cb..d17db250d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/ReservedWordParameters.swift @@ -1 +1,23 @@ @JS func check(extension: Int, break: Double, function: Bool, in: String) {} +@JS func checkEscaped(_ `in`: String) {} + +@JS class ReservedWordBox { + @JS init(in: Int) {} + @JS func run(for: Int) {} + @JS static func make(default: Int) -> Int { 0 } +} + +@JS protocol ReservedWordDelegate { + func handle(in: Int, for: String, _ `default`: Bool) +} + +@JSFunction func importedCheck(in: Int, for: String, _ `default`: Bool) throws(JSException) + +@JS func checkEscapedCollision(in: Int, _in: Int) {} +@JS func checkGlueNameCollision(instance: Int, swift: String) {} + +@JS protocol BacktickedLabelDelegate { + func handle(`in`: Int) +} + +@JSFunction func importedSelfClosure(_ `self`: @escaping () -> Void) throws(JSException) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json index c27a4eb46..380de8738 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.json @@ -4,7 +4,101 @@ ], "classes" : [ + { + "constructor" : { + "abiName" : "bjs_ReservedWordBox_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "in", + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ] + }, + "methods" : [ + { + "abiName" : "bjs_ReservedWordBox_run", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "run", + "parameters" : [ + { + "label" : "for", + "name" : "for", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + } + } + }, + { + "abiName" : "bjs_ReservedWordBox_static_make", + "effects" : { + "isAsync" : false, + "isStatic" : true, + "isThrows" : false + }, + "name" : "make", + "parameters" : [ + { + "label" : "default", + "name" : "default", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + }, + "staticContext" : { + "className" : { + "_0" : "ReservedWordBox" + } + } + } + ], + "name" : "ReservedWordBox", + "properties" : [ + + ], + "swiftCallName" : "ReservedWordBox" + } ], "enums" : [ @@ -63,17 +157,301 @@ "returnType" : { "void" : { + } + } + }, + { + "abiName" : "bjs_checkEscaped", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "checkEscaped", + "parameters" : [ + { + "label" : "_", + "name" : "in", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_checkEscapedCollision", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "checkEscapedCollision", + "parameters" : [ + { + "label" : "in", + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "_in", + "name" : "_in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_checkGlueNameCollision", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "checkGlueNameCollision", + "parameters" : [ + { + "label" : "instance", + "name" : "instance", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "swift", + "name" : "swift", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + } } } ], "protocols" : [ + { + "methods" : [ + { + "abiName" : "bjs_ReservedWordDelegate_handle", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "handle", + "parameters" : [ + { + "label" : "in", + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "for", + "name" : "for", + "type" : { + "string" : { + + } + } + }, + { + "label" : "_", + "name" : "default", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "ReservedWordDelegate", + "properties" : [ + ] + }, + { + "methods" : [ + { + "abiName" : "bjs_BacktickedLabelDelegate_handle", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "handle", + "parameters" : [ + { + "label" : "`in`", + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "BacktickedLabelDelegate", + "properties" : [ + + ] + } ], "structs" : [ ] }, + "imported" : { + "children" : [ + { + "functions" : [ + { + "accessLevel" : "internal", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : true + }, + "name" : "importedCheck", + "parameters" : [ + { + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "name" : "for", + "type" : { + "string" : { + + } + } + }, + { + "name" : "default", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "accessLevel" : "internal", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : true + }, + "name" : "importedSelfClosure", + "parameters" : [ + { + "name" : "self", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "10TestModuley_y", + "moduleName" : "TestModule", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "types" : [ + + ] + } + ] + }, "moduleName" : "TestModule", "usedExternalModules" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift index b09e1a76d..51b65629f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ReservedWordParameters.swift @@ -1,3 +1,122 @@ +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_TestModule_10TestModuley_y") +fileprivate func invoke_js_callback_TestModule_10TestModuley_y_extern(_ callback: Int32) -> Void +#else +fileprivate func invoke_js_callback_TestModule_10TestModuley_y_extern(_ callback: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func invoke_js_callback_TestModule_10TestModuley_y(_ callback: Int32) -> Void { + return invoke_js_callback_TestModule_10TestModuley_y_extern(callback) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_TestModule_10TestModuley_y") +fileprivate func make_swift_closure_TestModule_10TestModuley_y_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_TestModule_10TestModuley_y_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func make_swift_closure_TestModule_10TestModuley_y(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + return make_swift_closure_TestModule_10TestModuley_y_extern(boxPtr, file, line) +} + +private enum _BJS_Closure_10TestModuley_y { + static func bridgeJSLift(_ callbackId: Int32) -> () -> Void { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] in + #if arch(wasm32) + let callbackValue = callback.bridgeJSLowerParameter() + invoke_js_callback_TestModule_10TestModuley_y(callbackValue) + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == () -> Void { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping () -> Void) { + self.init( + makeClosure: make_swift_closure_TestModule_10TestModuley_y, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_TestModule_10TestModuley_y") +@_cdecl("invoke_swift_closure_TestModule_10TestModuley_y") +public func _invoke_swift_closure_TestModule_10TestModuley_y(_ boxPtr: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<() -> Void>>.fromOpaque(boxPtr).takeUnretainedValue().closure + closure() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension ReservedWordDelegate where Self: _BridgedSwiftProtocolWrapper { + func handle(`in`: Int, `for`: String, _ `default`: Bool) -> Void { + `for`.bridgeJSWithLoweredParameter { (forWord0, forWord1, forWord2) in + let defaultValue = `default`.bridgeJSLowerParameter() + let inValue = `in`.bridgeJSLowerParameter() + let jsObjectValue = jsObject.bridgeJSLowerParameter() + bjs_ReservedWordDelegate_handle(jsObjectValue, inValue, forWord0, forWord1, forWord2, defaultValue) + } + } +} + +struct AnyReservedWordDelegate: ReservedWordDelegate, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyReservedWordDelegate(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_ReservedWordDelegate_handle") +fileprivate func bjs_ReservedWordDelegate_handle_extern(_ jsObject: Int32, _ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Void +#else +fileprivate func bjs_ReservedWordDelegate_handle_extern(_ jsObject: Int32, _ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_ReservedWordDelegate_handle(_ jsObject: Int32, _ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Void { + return bjs_ReservedWordDelegate_handle_extern(jsObject, `in`, forWord0, forWord1, forWord2, `default`) +} + +extension BacktickedLabelDelegate where Self: _BridgedSwiftProtocolWrapper { + func handle(`in`: Int) -> Void { + let inValue = `in`.bridgeJSLowerParameter() + let jsObjectValue = jsObject.bridgeJSLowerParameter() + bjs_BacktickedLabelDelegate_handle(jsObjectValue, inValue) + } +} + +struct AnyBacktickedLabelDelegate: BacktickedLabelDelegate, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyBacktickedLabelDelegate(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_BacktickedLabelDelegate_handle") +fileprivate func bjs_BacktickedLabelDelegate_handle_extern(_ jsObject: Int32, _ `in`: Int32) -> Void +#else +fileprivate func bjs_BacktickedLabelDelegate_handle_extern(_ jsObject: Int32, _ `in`: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_BacktickedLabelDelegate_handle(_ jsObject: Int32, _ `in`: Int32) -> Void { + return bjs_BacktickedLabelDelegate_handle_extern(jsObject, `in`) +} + @_expose(wasm, "bjs_check") @_cdecl("bjs_check") public func _bjs_check(_ `extension`: Int32, _ `break`: Float64, _ function: Int32, _ inBytes: Int32, _ inLength: Int32) -> Void { @@ -10,4 +129,152 @@ public func _bjs_check(_ `extension`: Int32, _ `break`: Float64, _ function: Int #else fatalError("Only available on WebAssembly") #endif +} + +@_expose(wasm, "bjs_checkEscaped") +@_cdecl("bjs_checkEscaped") +public func _bjs_checkEscaped(_ inBytes: Int32, _ inLength: Int32) -> Void { + #if arch(wasm32) + let `in` = String.bridgeJSLiftParameter(inBytes, inLength) + checkEscaped(_: `in`) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_checkEscapedCollision") +@_cdecl("bjs_checkEscapedCollision") +public func _bjs_checkEscapedCollision(_ `in`: Int32, _ _in: Int32) -> Void { + #if arch(wasm32) + let _in = Int.bridgeJSLiftParameter(_in) + let `in` = Int.bridgeJSLiftParameter(`in`) + checkEscapedCollision(in: `in`, _in: _in) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_checkGlueNameCollision") +@_cdecl("bjs_checkGlueNameCollision") +public func _bjs_checkGlueNameCollision(_ instance: Int32, _ swiftBytes: Int32, _ swiftLength: Int32) -> Void { + #if arch(wasm32) + let swift = String.bridgeJSLiftParameter(swiftBytes, swiftLength) + let instance = Int.bridgeJSLiftParameter(instance) + checkGlueNameCollision(instance: instance, swift: swift) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ReservedWordBox_init") +@_cdecl("bjs_ReservedWordBox_init") +public func _bjs_ReservedWordBox_init(_ `in`: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) + let `in` = Int.bridgeJSLiftParameter(`in`) + let ret = ReservedWordBox(in: `in`) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ReservedWordBox_run") +@_cdecl("bjs_ReservedWordBox_run") +public func _bjs_ReservedWordBox_run(_ _self: UnsafeMutableRawPointer, _ `for`: Int32) -> Void { + #if arch(wasm32) + let `for` = Int.bridgeJSLiftParameter(`for`) + let _self = ReservedWordBox.bridgeJSLiftParameter(_self) + _self.run(for: `for`) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ReservedWordBox_static_make") +@_cdecl("bjs_ReservedWordBox_static_make") +public func _bjs_ReservedWordBox_static_make(_ `default`: Int32) -> Int32 { + #if arch(wasm32) + let `default` = Int.bridgeJSLiftParameter(`default`) + let ret = ReservedWordBox.make(default: `default`) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_ReservedWordBox_deinit") +@_cdecl("bjs_ReservedWordBox_deinit") +public func _bjs_ReservedWordBox_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension ReservedWordBox: ConvertibleToJSValue, _BridgedSwiftHeapObject, _BridgedSwiftProtocolExportable { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_ReservedWordBox_wrap(Unmanaged.passRetained(self).toOpaque())))) + } + consuming func bridgeJSLowerAsProtocolReturn() -> Int32 { + _bjs_ReservedWordBox_wrap(Unmanaged.passRetained(self).toOpaque()) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_ReservedWordBox_wrap") +fileprivate func _bjs_ReservedWordBox_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_ReservedWordBox_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_ReservedWordBox_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + return _bjs_ReservedWordBox_wrap_extern(pointer) +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_importedCheck") +fileprivate func bjs_importedCheck_extern(_ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Void +#else +fileprivate func bjs_importedCheck_extern(_ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_importedCheck(_ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Void { + return bjs_importedCheck_extern(`in`, forWord0, forWord1, forWord2, `default`) +} + +func _$importedCheck(_ `in`: Int, _ `for`: String, _ `default`: Bool) throws(JSException) -> Void { + `for`.bridgeJSWithLoweredParameter { (forWord0, forWord1, forWord2) in + let defaultValue = `default`.bridgeJSLowerParameter() + let inValue = `in`.bridgeJSLowerParameter() + bjs_importedCheck(inValue, forWord0, forWord1, forWord2, defaultValue) + } + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_importedSelfClosure") +fileprivate func bjs_importedSelfClosure_extern(_ self: Int32) -> Void +#else +fileprivate func bjs_importedSelfClosure_extern(_ self: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_importedSelfClosure(_ self: Int32) -> Void { + return bjs_importedSelfClosure_extern(self) +} + +func _$importedSelfClosure(_ self: @escaping () -> Void) throws(JSException) -> Void { + let `self` = JSTypedClosure<() -> Void>(self) + let selfFuncRef = self.bridgeJSLowerParameter() + withExtendedLifetime((self)) { + bjs_importedSelfClosure(selfFuncRef) + } + if let error = _swift_js_take_exception() { + throw error + } } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts index 027a6b9db..82dc96f71 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.d.ts @@ -4,10 +4,37 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. +export interface ReservedWordDelegate { + handle(_in: number, _for: string, _default: boolean): void; +} + +export interface BacktickedLabelDelegate { + handle(_in: number): void; +} + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +export interface ReservedWordBox extends SwiftHeapObject { + run(_for: number): void; +} export type Exports = { check(extension: number, _break: number, _function: boolean, _in: string): void; + checkEscaped(_in: string): void; + checkEscapedCollision(_in: number, _in1: number): void; + checkGlueNameCollision(instance: number, swift: string): void; + ReservedWordBox: { + new(_in: number): ReservedWordBox; + make(_default: number): number; + }, } export type Imports = { + importedCheck(_in: number, _for: string, _default: boolean): void; + importedSelfClosure(self: () => void): void; } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js index 0abe5fdbf..c5e67f8ae 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReservedWordParameters.js @@ -33,6 +33,31 @@ export async function createInstantiator(options, swift) { let _exports = null; let bjs = null; + const swiftClosureRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.unregistered) { return; } + instance?.exports?.bjs_release_swift_closure(state.pointer); + }); + const makeClosure = (pointer, file, line, func) => { + const state = { pointer, file, line, unregistered: false }; + const real = (...args) => { + if (state.unregistered) { + const bytes = new Uint8Array(memory.buffer, state.file >>> 0); + let length = 0; + while (bytes[length] !== 0) { length += 1; } + const fileID = decodeUTF8(state.file, length); + throw new Error(`Attempted to call a released JSTypedClosure created at ${fileID}:${state.line}`); + } + return func(...args); + }; + real.__unregister = () => { + if (state.unregistered) { return; } + state.unregistered = true; + swiftClosureRegistry.unregister(state); + }; + swiftClosureRegistry.register(real, state, state); + return swift.memory.retain(real); + }; + return { /** @@ -41,6 +66,7 @@ export async function createInstantiator(options, swift) { addImports: (importObject, importsContext) => { bjs = {}; importObject["bjs"] = bjs; + const imports = options.getImports(importsContext); bjs["swift_js_return_string"] = function(word0, word1, word2) { tmpRetString = decodeString(word0, word1, word2); } @@ -206,6 +232,69 @@ export async function createInstantiator(options, swift) { return pointer || 0; } bjs["swift_js_closure_unregister"] = function(funcRef) {} + bjs["swift_js_closure_unregister"] = function(funcRef) { + const func = swift.memory.getObject(funcRef); + func.__unregister(); + } + bjs["invoke_js_callback_TestModule_10TestModuley_y"] = function(callbackId) { + try { + const callback = swift.memory.getObject(callbackId); + callback(); + } catch (error) { + setException(error); + } + } + bjs["make_swift_closure_TestModule_10TestModuley_y"] = function(boxPtr, file, line) { + const lower_closure_TestModule_10TestModuley_y = function() { + instance.exports.invoke_swift_closure_TestModule_10TestModuley_y(boxPtr); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + }; + return makeClosure(boxPtr, file, line, lower_closure_TestModule_10TestModuley_y); + } + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_ReservedWordBox_wrap"] = function(pointer) { + const obj = _exports['ReservedWordBox'].__construct(pointer); + return swift.memory.retain(obj); + }; + const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; + TestModule["bjs_importedCheck"] = function bjs_importedCheck(_in, forWord0, forWord1, forWord2, _default) { + try { + const string = decodeString(forWord0, forWord1, forWord2); + imports.importedCheck(_in, string, _default !== 0); + } catch (error) { + setException(error); + } + } + TestModule["bjs_importedSelfClosure"] = function bjs_importedSelfClosure(self) { + try { + imports.importedSelfClosure(swift.memory.getObject(self)); + } catch (error) { + setException(error); + } + } + TestModule["bjs_ReservedWordDelegate_handle"] = function bjs_ReservedWordDelegate_handle(self, _in, forWord0, forWord1, forWord2, _default) { + try { + const string = decodeString(forWord0, forWord1, forWord2); + swift.memory.getObject(self).handle(_in, string, _default !== 0); + } catch (error) { + setException(error); + } + } + TestModule["bjs_BacktickedLabelDelegate_handle"] = function bjs_BacktickedLabelDelegate_handle(self, _in) { + try { + swift.memory.getObject(self).handle(_in); + } catch (error) { + setException(error); + } + } }, setInstance: (i) => { if (memory !== i.exports.memory) immortalStrings.clear(); @@ -368,12 +457,95 @@ export async function createInstantiator(options, swift) { /** @param {WebAssembly.Instance} instance */ createExports: (instance) => { const js = swift.memory.heap; + const swiftHeapObjectFinalizationRegistry = (typeof FinalizationRegistry === "undefined") ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((state) => { + if (state.hasReleased) { + return; + } + state.hasReleased = true; + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + }); + + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype, identityCache) { + pointer = pointer >>> 0; + const makeFresh = (identityMap) => { + const obj = Object.create(prototype); + const state = { pointer, deinit, hasReleased: false, identityMap }; + obj.pointer = pointer; + obj.__swiftHeapObjectState = state; + swiftHeapObjectFinalizationRegistry.register(obj, state, state); + if (identityMap) { + identityMap.set(pointer, new WeakRef(obj)); + } + return obj; + }; + + if (!identityCache) { + return makeFresh(null); + } + + const cached = identityCache.get(pointer)?.deref(); + if (cached && !cached.__swiftHeapObjectState.hasReleased) { + deinit(pointer); + return cached; + } + if (identityCache.has(pointer)) { + identityCache.delete(pointer); + } + + return makeFresh(identityCache); + } + + release() { + const state = this.__swiftHeapObjectState; + if (state.hasReleased) { + return; + } + state.hasReleased = true; + swiftHeapObjectFinalizationRegistry.unregister(state); + state.identityMap?.delete(state.pointer); + state.deinit(state.pointer); + } + } + class ReservedWordBox extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_ReservedWordBox_deinit, ReservedWordBox.prototype, null); + } + + constructor(_in) { + const ret = instance.exports.bjs_ReservedWordBox_init(_in); + return ReservedWordBox.__construct(ret); + } + run(_for) { + instance.exports.bjs_ReservedWordBox_run(this.pointer, _for); + } + static make(_default) { + const ret = instance.exports.bjs_ReservedWordBox_static_make(_default); + return ret; + } + } const exports = { check: function bjs_check(extension, _break, _function, _in) { const _inBytes = textEncoder.encode(_in); const _inId = swift.memory.retain(_inBytes); instance.exports.bjs_check(extension, _break, _function, _inId, _inBytes.length); }, + checkEscaped: function bjs_checkEscaped(_in) { + const _inBytes = textEncoder.encode(_in); + const _inId = swift.memory.retain(_inBytes); + instance.exports.bjs_checkEscaped(_inId, _inBytes.length); + }, + checkEscapedCollision: function bjs_checkEscapedCollision(_in, _in1) { + instance.exports.bjs_checkEscapedCollision(_in, _in1); + }, + checkGlueNameCollision: function bjs_checkGlueNameCollision(instance1, swift1) { + const swift1Bytes = textEncoder.encode(swift1); + const swift1Id = swift.memory.retain(swift1Bytes); + instance.exports.bjs_checkGlueNameCollision(instance1, swift1Id, swift1Bytes.length); + }, + ReservedWordBox, }; _exports = exports; return exports; diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 9e5892a87..1429bb0fb 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -25,6 +25,18 @@ func runJsWorks() -> Void @JS func describeReservedWordParameters(extension: Int, break: Double, function: Bool, in: String) -> String { return "\(`extension`):\(`break`):\(function):\(`in`)" } +@JS func describeBacktickedParameter(_ `in`: String) -> String { + return `in` +} +@JS func describeCollidingParameters(in: Int, _in: Int, instance: Int, swift: String) -> String { + return "\(`in`):\(_in):\(instance):\(swift)" +} +@JS protocol ReservedWordDescriber { + func describe(in: Int, for: String) -> String +} +@JS func describeWithReservedWordDescriber(_ describer: any ReservedWordDescriber) -> String { + return describer.describe(in: 1, for: "for") +} @JS func roundTripSwiftHeapObject(v: Greeter) -> Greeter { return v } diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 884ecb8e1..f2c868c84 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -4294,6 +4294,39 @@ fileprivate func bjs_ArrayElementProtocol_value_set_extern(_ jsObject: Int32, _ return bjs_ArrayElementProtocol_value_set_extern(jsObject, newValue) } +extension ReservedWordDescriber where Self: _BridgedSwiftProtocolWrapper { + func describe(`in`: Int, `for`: String) -> String { + let ret0 = `for`.bridgeJSWithLoweredParameter { (forWord0, forWord1, forWord2) in + let inValue = `in`.bridgeJSLowerParameter() + let jsObjectValue = jsObject.bridgeJSLowerParameter() + let ret = bjs_ReservedWordDescriber_describe(jsObjectValue, inValue, forWord0, forWord1, forWord2) + return ret + } + let ret = ret0 + return String.bridgeJSLiftReturn(ret) + } +} + +struct AnyReservedWordDescriber: ReservedWordDescriber, _BridgedSwiftProtocolWrapper { + let jsObject: JSObject + + static func bridgeJSLiftParameter(_ value: Int32) -> Self { + return AnyReservedWordDescriber(jsObject: JSObject(id: UInt32(bitPattern: value))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_ReservedWordDescriber_describe") +fileprivate func bjs_ReservedWordDescriber_describe_extern(_ jsObject: Int32, _ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32) -> Int32 +#else +fileprivate func bjs_ReservedWordDescriber_describe_extern(_ jsObject: Int32, _ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_ReservedWordDescriber_describe(_ jsObject: Int32, _ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32) -> Int32 { + return bjs_ReservedWordDescriber_describe_extern(jsObject, `in`, forWord0, forWord1, forWord2) +} + extension DataProcessor where Self: _BridgedSwiftProtocolWrapper { func increment(by amount: Int) -> Void { let amountValue = amount.bridgeJSLowerParameter() @@ -10339,6 +10372,45 @@ public func _bjs_describeReservedWordParameters(_ `extension`: Int32, _ `break`: #endif } +@_expose(wasm, "bjs_describeBacktickedParameter") +@_cdecl("bjs_describeBacktickedParameter") +public func _bjs_describeBacktickedParameter(_ inBytes: Int32, _ inLength: Int32) -> Void { + #if arch(wasm32) + let `in` = String.bridgeJSLiftParameter(inBytes, inLength) + let ret = describeBacktickedParameter(_: `in`) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_describeCollidingParameters") +@_cdecl("bjs_describeCollidingParameters") +public func _bjs_describeCollidingParameters(_ `in`: Int32, _ _in: Int32, _ instance: Int32, _ swiftBytes: Int32, _ swiftLength: Int32) -> Void { + #if arch(wasm32) + let swift = String.bridgeJSLiftParameter(swiftBytes, swiftLength) + let instance = Int.bridgeJSLiftParameter(instance) + let _in = Int.bridgeJSLiftParameter(_in) + let `in` = Int.bridgeJSLiftParameter(`in`) + let ret = describeCollidingParameters(in: `in`, _in: _in, instance: instance, swift: swift) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_describeWithReservedWordDescriber") +@_cdecl("bjs_describeWithReservedWordDescriber") +public func _bjs_describeWithReservedWordDescriber(_ describer: Int32) -> Void { + #if arch(wasm32) + let describer = AnyReservedWordDescriber.bridgeJSLiftParameter(describer) + let ret = describeWithReservedWordDescriber(_: describer) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_roundTripSwiftHeapObject") @_cdecl("bjs_roundTripSwiftHeapObject") public func _bjs_roundTripSwiftHeapObject(_ v: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { @@ -19975,6 +20047,32 @@ func _$jsJoinStringThenStackParams(_ s: String, _ a: Optional<[Int]>, _ b: [Int] return String.bridgeJSLiftReturn(ret) } +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsDescribeReservedWordParameters") +fileprivate func bjs_jsDescribeReservedWordParameters_extern(_ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Int32 +#else +fileprivate func bjs_jsDescribeReservedWordParameters_extern(_ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func bjs_jsDescribeReservedWordParameters(_ `in`: Int32, _ forWord0: Int32, _ forWord1: Int32, _ forWord2: Int32, _ `default`: Int32) -> Int32 { + return bjs_jsDescribeReservedWordParameters_extern(`in`, forWord0, forWord1, forWord2, `default`) +} + +func _$jsDescribeReservedWordParameters(_ `in`: Int, _ `for`: String, _ `default`: Bool) throws(JSException) -> String { + let ret0 = `for`.bridgeJSWithLoweredParameter { (forWord0, forWord1, forWord2) in + let defaultValue = `default`.bridgeJSLowerParameter() + let inValue = `in`.bridgeJSLowerParameter() + let ret = bjs_jsDescribeReservedWordParameters(inValue, forWord0, forWord1, forWord2, defaultValue) + return ret + } + let ret = ret0 + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) +} + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsGenericRoundTrip") fileprivate func bjs_jsGenericRoundTrip_extern(_ _generic0TypeId: Int32) -> Void diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 8598361b6..265fcf9cb 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -15269,6 +15269,117 @@ } } }, + { + "abiName" : "bjs_describeBacktickedParameter", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "describeBacktickedParameter", + "parameters" : [ + { + "label" : "_", + "name" : "in", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_describeCollidingParameters", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "describeCollidingParameters", + "parameters" : [ + { + "label" : "in", + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "_in", + "name" : "_in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "instance", + "name" : "instance", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "swift", + "name" : "swift", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_describeWithReservedWordDescriber", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "describeWithReservedWordDescriber", + "parameters" : [ + { + "label" : "_", + "name" : "describer", + "type" : { + "swiftProtocol" : { + "_0" : "ReservedWordDescriber" + } + } + } + ], + "returnType" : { + "string" : { + + } + } + }, { "abiName" : "bjs_roundTripSwiftHeapObject", "effects" : { @@ -19314,6 +19425,51 @@ } ] }, + { + "methods" : [ + { + "abiName" : "bjs_ReservedWordDescriber_describe", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "describe", + "parameters" : [ + { + "label" : "in", + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "label" : "for", + "name" : "for", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + } + ], + "name" : "ReservedWordDescriber", + "properties" : [ + + ] + }, { "methods" : [ { @@ -26306,6 +26462,49 @@ "returnType" : { "string" : { + } + } + }, + { + "accessLevel" : "internal", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : true + }, + "name" : "jsDescribeReservedWordParameters", + "parameters" : [ + { + "name" : "in", + "type" : { + "integer" : { + "_0" : { + "isSigned" : true, + "width" : "word" + } + } + } + }, + { + "name" : "for", + "type" : { + "string" : { + + } + } + }, + { + "name" : "default", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "string" : { + } } } diff --git a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift index c98ab3e61..b3e921672 100644 --- a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift @@ -31,6 +31,12 @@ import JavaScriptKit _ b: [Int] ) throws(JSException) -> String +@JSFunction func jsDescribeReservedWordParameters( + in: Int, + for: String, + _ `default`: Bool +) throws(JSException) -> String + class ImportAPITests: XCTestCase { func testRoundTripVoid() throws { try jsRoundTripVoid() @@ -211,4 +217,8 @@ class ImportAPITests: XCTestCase { #""s"|[1,2]|[7,8,9]"# ) } + + func testReservedWordParameters() throws { + XCTAssertEqual(try jsDescribeReservedWordParameters(in: 1, for: "for", true), "1:for:true") + } } diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 15631bcbe..17b48679c 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -164,6 +164,7 @@ export async function setupOptions(options, context) { jsJoinOptionalStructThenArray: joinStackParams, jsJoinEnumThenArray: joinStackParams, jsJoinStringThenStackParams: joinStackParams, + jsDescribeReservedWordParameters: (a, b, c) => `${a}:${b}:${c}`, jsGenericRoundTrip: (v) => v, jsGenericRoundTripClass: (v) => v, jsGenericParsePoint: (json) => JSON.parse(json), @@ -306,6 +307,12 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.equal(exports.roundTripString(v), v); } assert.equal(exports.describeReservedWordParameters(1, 2.5, true, "in"), "1:2.5:true:in"); + assert.equal(exports.describeBacktickedParameter("in"), "in"); + assert.equal(exports.describeCollidingParameters(1, 2, 3, "swift"), "1:2:3:swift"); + assert.equal( + exports.describeWithReservedWordDescriber({ describe: (a, b) => `${a}:${b}` }), + "1:for" + ); const dict = { a: 1, b: 2 }; assert.deepEqual(exports.roundTripDictionaryExport(dict), dict); const optDict = { hello: "world" };