NFC: BridgeJS: Descriptor-driven codegen, remove dead cleanup infrastructure#622
NFC: BridgeJS: Descriptor-driven codegen, remove dead cleanup infrastructure#622krodak wants to merge 1 commit intoswiftwasm:mainfrom
Conversation
771bd03 to
5726968
Compare
|
I think the schema-driven approach makes sense to me. On the other hand, I think what we really need to think more about is how to composite the type descriptors. e.g. Optional should have a different descriptor depending on wrapped type T, so we need to define a general rule for that. Or define a fallback convention that works without knowing the T's descriptor by using Stack ABI and define specialized descriptors for known cases. I still haven't checked the entire changes yet so I might be missing something 🙇 |
|
@kateinoigakukun thanks for looking at this, I'll think on your feedback and try to progress in this direction when I can; in parallel I'll look for more simplifications around intrinsics like we are both currently doing 👌🏻 |
df5af2e to
1646876
Compare
|
@kateinoigakukun updated PR description and made some more changes and fixes, no rush on this, but PR should be in a good to have a look for further discussion; let me know if some of the changes would satisfy your earlier remarks partially |
2536334 to
30f2b6f
Compare
30f2b6f to
45d949d
Compare
…anup infrastructure
45d949d to
1656b22
Compare
|
@kateinoigakukun cleaned up code around clean up, did some smaller improvements, not sure I like final form honestly, but no further better ideas for direction. I know one of the most problematic parts for me initially was to work amongst dispersed |
Context
Following #496 - @kateinoigakukun raised a concern about adding more
BridgeTypecases before paying down complexity debt in codegen. The ad-hoc per-type handling should be moved out of codegen, and extending supported types shouldn't require invasive changes.In my reply, I suggested defining pairs of Swift/TS type bridging declaratively and generalizing codegen logic.
This PR is an attempt at that. Open to feedback - we can rework it, split parts out, or close it entirely. The goal is to centralize type-specific ABI knowledge so codegen operates generically rather than switching on every type.
The idea
Today, each codegen function (
lowerParameter,liftReturn,optionalLowerReturn, etc.) has its ownswitchoverBridgeTypeencoding the same structural facts in slightly different ways. Adding a new simple type means touching many of these switches.This PR introduces four declarative abstractions:
BridgeTypeDescriptorDefined once per
BridgeType, captures the Wasm ABI shape:wasmParams/importParams- parameter types for export vs import directionwasmReturnType/importReturnType- return type per directionoptionalConvention- howOptional<T>is handled (see below), explicitly declared on every descriptornilSentinel- bit pattern representingnilfor types with extra inhabitants (see below)usesStackLifting- whether multi-param stack lifting needs LIFO reorderingaccessorTransform- how to access the bridgeable value from a Swift accessor (identity,.jsObjectmember, protocol downcast)lowerMethod- which bridge protocol method to call (stackReturn, fullReturn, pushParameter, none)Codegen reads descriptor fields instead of switching per type.
lowerStatementsusesdescriptor.lowerMethodanddescriptor.accessorTransformrather than matching on.jsObject,.swiftProtocol,.swiftStruct, etc. individually. Return-type switches inrenderCallStatement,callStaticProperty, andcallPropertyGetterare unified viaaccessorTransform.applyToReturnBinding().OptionalConventionHow
Optional<T>is lowered/lifted for a given wrapped type:Every descriptor explicitly declares its convention - no implicit derivation from
wasmParamscount.NilSentinelThe
NilSentinelenum captures whether a type has a value that can representnilwithout an extraisSomeflag:Types with sentinels:
jsObject,swiftProtocol- sentinel 0 (object IDs start at 2)swiftHeapObject- sentinel null pointercaseEnum,associatedValueEnum- sentinel -1 (never a valid case index)The sentinel is used in
optionalLowerReturnfor the JS import direction - types with sentinels use a generic sentinel-based return path instead of per-type switch cases. The innerlowerReturnfragment is composed into anisSome ? <lowered> : <sentinel>pattern automatically.JSCoercionJS-side coercion info for simple scalar types:
OptionalScalarKindderives the side-channel storage name and intrinsic function name from the case, replacing the previous string-based fields.Types with non-nil
jsCoerciongo through a genericscalarFragments()builder that returns a(lift, lower)pair. This removes all per-type scalar fragment functions (boolLowerParameter,uintLiftReturn, etc.).Compositional optional handling
optionalLowerParameterandoptionalLiftParameterno longer contain per-type switches. They compose T's existing fragment inside anisSomeconditional:optionalLowerParameter: Runs T'slowerParameterinto a buffer printer, captures results into outer variables. The buffer detects whether the inner fragment has side effects and chooses inline ternary vs if/else block.optionalLiftParameter: Runs T'sliftParameterinto a buffer. Empty buffer (pure expression) uses a ternary; side effects get an if/else block.New types added to
lowerParameterorliftParameterget correct optional handling for free. Same applies to struct fields viastructFieldLowerFragment.Dead cleanup infrastructure removal
After #635 and #636, no type produces cleanup code at runtime., thus related code is removed.
Import/export param unification via
importParamsTypes like
stringandswiftStructhave different parameter shapes by direction (export string =(bytes, length), import = single object ID). TheimportParamsdescriptor field captures this.loweringParameterInforeadsimportParams,liftParameterInforeadswasmParams, eliminating the per-type parameter info switches.liftExpressioncollapseIn
StackCodegen.liftExpression, 15 types all generatedTypeName.bridgeJSLiftParameter()- collapsed to adefaultcase. Similarly,liftNullableExpressioncollapsed from 15 explicit cases to 2.What's not fully unified (and why)
Some per-type logic remains where types have genuinely different codegen:
optionalLiftReturnandoptionalLowerReturnstill switch per type - these are genuinely heterogeneous (side-channel storage, stack flags, different JS APIs)stackLiftFragmentandstackLowerFragmentswitch on ~7 complex types with type-specific associated data (class names, struct base names) that make them irreducible. The scalar path already goes throughjsCoercion.What adding a new type (e.g. UUID) would look like
.uuidtoBridgeTypeBridgeJSLowerStackReturn/BridgeJSLiftParameter)Foundation.UUIDCodegen picks up the new type automatically through the descriptor. Same pattern applies to URL (string on the wire via
.absoluteString/URL(string:)!). Foundation-gating (#if canImport(Foundation)) goes in the runtime conformances for Embedded Swift compatibility.Other generated code improvements
__bjs_jsValueLowerno longer called on null values; theisSomeguard runs before lowering, with typed defaults in the else branch() => { return () => ({...}); }simplified to() => ({...}); call-site()()becomes()0.0for floats,flag ? 1 : 0for bools (previously0for everything)param0Valuerenamed toparam0Pointerfor struct/enum pointer parameters