Skip to content

Conversation

krodak
Copy link
Member

@krodak krodak commented Oct 2, 2025

Introduction

This PR adds support for default parameter values when exporting Swift functions and constructors to JavaScript/TypeScript using the BridgeJS plugin.

Resolves: #452

Overview

  • Default parameters are represented as optional parameters (param?: type) in TypeScript, using undefined to trigger default value application, while not conflict with support for optionals
  • Generated JavaScript uses native default parameter syntax
  • JSDoc comments automatically document default values in TypeScript definitions
  • Supported defaults: literals, nil, enum cases, and object initialization (e.g. MyClass("arg", 42))
  • There are some additional checks for unsupported expression types used as default values, so those should result in clear error (checks for method calls, closures, unsupported types, etc)

Examples

@JS public func greet(name: String = "World", enthusiastic: Bool = false) -> String {
    return enthusiastic ? "Hello, \(name)!" : "Hello, \(name)"
}

@JS class Config {
    @JS var name: String
    @JS var timeout: Int
    
    @JS init(name: String = "default", timeout: Int = 30) {
        self.name = name
        self.timeout = timeout
    }
}

Generated JavaScript:

export function greet(name = "World", enthusiastic = false) {
    ...
}

export class Config extends SwiftHeapObject {
    constructor(name = "default", timeout = 30) {
        ...
    }
}

Generated TypeScript:

export type Exports = {
    /**
     * @param name - Optional parameter (default: "World")
     * @param enthusiastic - Optional parameter (default: false)
     */
    greet(name?: string, enthusiastic?: boolean): string;
}

export interface Config extends SwiftHeapObject {
    /**
     * @param name - Optional parameter (default: "default")
     * @param timeout - Optional parameter (default: 30)
     */
    new(name?: string, timeout?: number): Config;
}

Testing

Added tests for different scenarios covering all supported default value types, including parameters, constructors, negative numbers, optionals, enums, and object initialization.

Documentation

Added documentation in Exporting-Swift-Default-Parameters.md.

@krodak krodak requested a review from kateinoigakukun October 2, 2025 15:47
@krodak krodak self-assigned this Oct 2, 2025
BridgeJS: Clean up
BridgeJS: Simplified syntax for defaults in .js
@krodak krodak force-pushed the feat/default-values branch from d78c276 to e144554 Compare October 2, 2025 16:00
Copy link
Member

@kateinoigakukun kateinoigakukun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, thanks for working on this!

I have some concerns about the current approach before we merge. The main issue is that serializing default values to JavaScript requires explicit support for each literal type, which could become difficult to maintain. It's also inherently limited to values with 1:1 JS mappings, and we're passing the actual default value across the boundary when we really only need 1 bit of information (was the argument provided?), resulting in less perf efficiency.

The approach we used for Optional support might work better here: pass an isSome flag for each parameter and evaluate defaults on the Swift side when isSome == false. The downside is we'd need to generate multiple call-sites for each combination of default values (2^N combinations for N parameters):

switch (nameIsSome, ageIsSome) {
case (true, true): foo(String.lift(...), Int.lift(...))
case (true, false): foo(String.lift(...))
case (false, true): foo(age: Int.lift(...))
case (false, false): foo()
}

This could potentially be improved if we start using Swift macros, but I haven't found a clean solution yet.

Given that users can achieve similar functionality using Optional parameters, I'm wondering if the complexity-to-benefit ratio justifies native default parameter support right now, and just asking users to use Optional parameters. What do you think?

@krodak
Copy link
Member Author

krodak commented Oct 6, 2025

Thanks for the thoughtful feedback! I appreciate the concerns about maintenance and performance. Before this implementation, I've considered alternative approaches, including generating multiple functions overloads or adding new flag similar to isSome to be able to also support Optionals with default values and keeping those two features separated, but eventually decided for current approach, mostly because of intended developer experience and allowing to keep existing Swift-side interface without changes, hence I'd like to make a case for keeping native default parameter support:

Developer Experience & Natural Swift Syntax

The current implementation allows developers to write natural Swift code that works seamlessly across the bridge:

@JS func greet(name: String = "Joe") -> String

This matches Swift's native semantics and requires no special accommodation for BridgeJS. The alternative (using String? with manual default resolution) forces developers to modify their Swift code specifically for bridging, which might contradict BridgeJS's goal of making Swift-JavaScript interop feel natural.

Migration Path for Existing Codebases

Most Swift codebases heavily favor default parameters over optionals and internal default value resolution in their API design.

For example, in our Khasm project, in order to properly support default values which are common in our model definition layer (which is one of the parts we share between different products, as business model definitions is a good start point to share between different range of products) we currently use JavaScriptKit along with a custom code generator that specifically handles default parameters.
This custom code generation layer exists because our business model layer is shared across multiple products and uses idiomatic Swift with default parameters. If we migrate to BridgeJS without native default parameter support, we'd need to maintain this (or some similar) additional code generation infrastructure to avoid rewriting our shared business model layer. This seems to defeat one of BridgeJS's main value propositions: reducing the barrier to adoption - projects migrating to BridgeJS shouldn't need to rewrite their Swift interfaces or maintain dual API surfaces.

I think use case where potential BridgeJS users already have a suite of Swift apps and some JS app they want to migrate, having ability to start with existing codebase without much modification on Swift side is valuable feature that could speed up initial migration especially for larger codebases.

Flexibility Over Forced Optimization

While the isSome approach is more efficient in data transfer (1 bit vs serialized value), the trade-offs are significant:

  1. Code explosion: The 2^N switch statement problem creates substantial WASM binary bloat. For a function with 5 default parameters, that's 32 switch cases vs. a single code path with JavaScript-side defaults.

For example, lots of models in our business layer require ~ 10 parameters, each supporting default value.
Now given around 80 models and some of them having more parameters, actual resolution of Swift glue code could be problematic.

  1. Combinatorial complexity: Mixing optionals with defaults becomes untenable. Consider:

    func foo(name: String? = "default") -> String

    This requires distinguishing three states (not provided, explicitly null, has value), which can't be represented with a single isSome flag. You'd need either a tri-state enum or two flags, leading to 3^N combinations.

  2. WASM binary size: This suggests the 2^N approach adds few % to WASM binary size for typical projects, it might directly impact load time, not 100% sure on that part tbh.

  3. Swift-side changes - adopting commonly shared interface on Swift side or providing another layer of abstraction or preparing code generator to handle that means more developer resources to adopt WASM migration.

The current implementation lets developers choose: use defaults for convenience, or use optionals for maximum efficiency. Maybe extending documentation could help developer guide this decision and understand tradeoffs of each solution.

Maintenance Concerns

The current implementation is already well-structured with the DefaultValue enum supporting common literal types. Complex expressions are already rejected during parsing with clear diagnostic messages, preventing maintenance issues before they arise. While it might make sense to support more complex default values, current support should support what is mostly used in business model layer.
Adding support for static functions could be valuable, but this indeed could end up as more complex implementation than needed for most usecases.

Performance Impact

I understand the optimization concern about serializing defaults. However :

  • it still improves performance comparing to not using BridgeJS and facilitating default values via custom code generators and using JavaScriptKit's JSValue wrapper with dynamic dispatch
  • it does not force developer to prefer default values over Optionals with Swift-side, so if performance side effects of default values is significant, developer is free to extend their Swift-side layer by additional abstraction for WASM build target and WASM app
  • introducing new bool flag or updating isSome to tri-state enum could result in much more WASM binary size increase from switch statement explosion, this could impact initial load time

Given above, would you consider keeping default parameter support with the current JavaScript-side approach?
I'm happy to add documentation warnings about potential performance implications, helping developers optimize where it matters while maintaining natural Swift syntax for the common case.

The goal for this extension is mostly to allow BridgeJS to adopt for existing Swift projects, while not restricting developers to optimize when needed (by omitting default values and using optionals).

@krodak krodak requested a review from kateinoigakukun October 6, 2025 09:57
Copy link
Member

@kateinoigakukun kateinoigakukun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the detailed explanation! If you have a lot of existing code with default parameter values, I think it makes sense to merge this so as not to block the adoption.

@kateinoigakukun kateinoigakukun merged commit c6353c4 into swiftwasm:main Oct 6, 2025
34 of 45 checks passed
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 default values
2 participants