Skip to content

BridgeJS: Add Dictionary support#581

Merged
kateinoigakukun merged 1 commit intomainfrom
katei/0bd3-bridgejs-support
Feb 5, 2026
Merged

BridgeJS: Add Dictionary support#581
kateinoigakukun merged 1 commit intomainfrom
katei/0bd3-bridgejs-support

Conversation

@kateinoigakukun
Copy link
Member

@kateinoigakukun kateinoigakukun commented Feb 5, 2026

Overview

Add full dictionary bridging to BridgeJS: generate [String: T] bindings, carry them through link/mangling/TS typing, and bridge optional/undefined cases at runtime.

Examples

// Import side
@JSFunction func takeDict(_ values: [String: Int]) -> [String: Int]
@JSFunction func nested(_ values: [String: [Double]]) -> [String: [Double]]
@JSFunction func optional(_ values: [String: String]?) -> [String: String]?
@JSFunction func undefined(_ values: JSUndefinedOr<[String: Bool]>) -> JSUndefinedOr<[String: Bool]>

// Export side
@JSExport class Store {
    @JSFunction var dict: [String: Int]
    @JSFunction func merge(_ incoming: [String: Int]?) -> [String: Int] {
        var result = dict
        incoming?.forEach { result[$0] = $1 }
        return result
    }
}

Implementation

  • Extend SwiftToSkeleton and BridgeType with a .dictionary case for [String: T]/Dictionary<String, T> and map to Record<string, …> in TS output.
  • Teach JS glue to lower/lift dictionary entries on the stack, including optional/JSUndefinedOr payloads, and refresh generated artifacts. Optional return lifting now handles dictionaries, and JSValue elements lower/lift on the stack.
  • Introduce _BridgedSwiftDictionaryStackType so dictionaries integrate with optional stack ABI, and add runtime fixtures/tests covering int/bool/double/JSObject/JSValue dictionaries plus nested/optional/undefined shapes, including export-side round trips.

Limitations

  • Keys must be String; other key types are unsupported.
  • Values must be stack-bridged; heap-only/side-channel types remain unsupported.
  • Entry order follows Object.entries and is not stable/sorted.

@kateinoigakukun kateinoigakukun changed the title Add BridgeJS dictionary runtime coverage BridgeJS: add dictionary bridging and runtime coverage Feb 5, 2026
@kateinoigakukun kateinoigakukun changed the title BridgeJS: add dictionary bridging and runtime coverage BridgeJS: add dictionary bridging Feb 5, 2026
@kateinoigakukun kateinoigakukun changed the title BridgeJS: add dictionary bridging BridgeJS: Add Dictionary support Feb 5, 2026
@krodak
Copy link
Member

krodak commented Feb 5, 2026

Hey @kateinoigakukun, found and fixed a couple bugs in the dictionary PR for optional dictionary handling on exports.

Bug 1: Missing Optional dictionary support in BridgeJSIntrinsics.swift

The Optional where Wrapped: _BridgedSwiftDictionaryStackType extension was incomplete. When you have @JS func foo(_ values: [String: String]?) -> [String: String]?, both the parameter lifting and return lowering paths need proper handling.

Added complete Optional support:

extension Optional where Wrapped: _BridgedSwiftDictionaryStackType {
    @_spi(BridgeJS) public consuming func bridgeJSLowerParameter() -> Int32 {
        switch consume self {
        case .none:
            return 0
        case .some(let dict):
            dict.bridgeJSLowerReturn()
            return 1
        }
    }

    @_spi(BridgeJS) public consuming func bridgeJSLowerReturn() {
        switch consume self {
        case .none:
            _swift_js_push_i32(0)
        case .some(let dict):
            dict.bridgeJSLowerReturn()
            _swift_js_push_i32(1)
        }
    }

    @_spi(BridgeJS) public static func bridgeJSLiftParameter(_ isSome: Int32) -> [String: Wrapped.DictionaryValue]? {
        if isSome == 0 {
            return nil
        }
        return Dictionary<String, Wrapped.DictionaryValue>.bridgeJSLiftParameter()
    }

    // ... plus bridgeJSLiftParameter() and bridgeJSLiftReturn()
}

Bug 2: Missing .dictionary case in JSGlueGen.swift

The optionalLiftReturn function in JSGlueGen.swift was missing the .dictionary case for lifting optional dictionary return values on the JS side. Added following the same pattern as .array:

case .dictionary(let valueType):
    let isSomeVar = scope.variable("isSome")
    printer.write("const \(isSomeVar) = \(JSGlueVariableScope.reservedTmpRetInts).pop();")
    printer.write("let \(resultVar);")
    printer.write("if (\(isSomeVar)) {")
    printer.indent {
        let dictLiftFragment = try! dictionaryLift(valueType: valueType)
        let liftResults = dictLiftFragment.printCode([], scope, printer, cleanupCode)
        if let liftResult = liftResults.first {
            printer.write("\(resultVar) = \(liftResult);")
        }
    }
    printer.write("} else {")
    printer.indent {
        printer.write("\(resultVar) = \(absenceLiteral);")
    }
    printer.write("}")

So result was falling back to default code path resulting in invalid behavior, now removed:

-                    const optResult = tmpRetString;
-                    tmpRetString = undefined;
+                    const isSome1 = tmpRetInts.pop();
+                    let optResult;
+                    if (isSome1) {
+                        const dictLen = tmpRetInts.pop();
+                        const dictResult = {};
+                        for (let i = 0; i < dictLen; i++) {
+                            const string = tmpRetStrings.pop();
+                            const string1 = tmpRetStrings.pop();
+                            dictResult[string1] = string;
+                        }
+                        optResult = dictResult;
+                    } else {
+                        optResult = null;
+                    }

Also: e2e export tests were missing

Added e2e tests for both non-optional and optional dictionary exports.

Changes are on krodak/dict-fix, feel free to integrate them; I'm quite sure we need bug 2 fix with glue code, for bug 1 maybe there is more elegant solution, but couldn't find one

You can validate the issue, by including e2e export tests and running the build 🙏🏻

@kateinoigakukun kateinoigakukun force-pushed the katei/0bd3-bridgejs-support branch from ee19ac2 to aa82380 Compare February 5, 2026 10:17
@kateinoigakukun
Copy link
Member Author

:shipit:

@kateinoigakukun kateinoigakukun merged commit 72ec08d into main Feb 5, 2026
11 checks passed
@kateinoigakukun kateinoigakukun deleted the katei/0bd3-bridgejs-support branch February 5, 2026 10:49
@kateinoigakukun kateinoigakukun linked an issue Feb 5, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BridgeJS] Support Dictionary as parameter/return type

2 participants