-
-
Notifications
You must be signed in to change notification settings - Fork 57
[BridgeJS] Support default values #453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BridgeJS] Support default values #453
Conversation
BridgeJS: Clean up BridgeJS: Simplified syntax for defaults in .js
d78c276 to
e144554
Compare
There was a problem hiding this 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?
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:
- 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.
-
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
isSomeflag. You'd need either a tri-state enum or two flags, leading to 3^N combinations. -
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.
-
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).
There was a problem hiding this 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.
c6353c4
into
swiftwasm:main
Uh oh!
There was an error while loading. Please reload this page.
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
param?: type) in TypeScript, usingundefinedto trigger default value application, while not conflict with support for optionalsnil, enum cases, and object initialization (e.g.MyClass("arg", 42))Examples
Generated JavaScript:
Generated TypeScript:
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.