@expo/expo-modules-macros-plugin 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +124 -58
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +0 -8
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +9 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +44 -27
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +40 -14
- package/package.json +1 -1
|
Binary file
|
|
@@ -2,14 +2,17 @@ import SwiftSyntax
|
|
|
2
2
|
|
|
3
3
|
/// A `@JS func` collected for **direct JSI binding**. Instead of describing the function with a
|
|
4
4
|
/// `Function(...)` / `AsyncFunction(...)` DSL entry that the runtime interprets per call, the enclosing
|
|
5
|
-
/// macro synthesizes a decorator (`_decorateModule` / `_decorateSharedObject`) that binds each such
|
|
5
|
+
/// macro synthesizes a decorator (`_decorateModule(object:)` / `_decorateSharedObject(prototype:)`) that binds each such
|
|
6
6
|
/// function into the JS object via the closure-taking `JavaScriptObject.setProperty(_:)`, with the
|
|
7
|
-
/// decode-call-encode body inlined into the closure. This omits the
|
|
7
|
+
/// decode-call-encode body inlined into the closure. This omits the dynamic-call path entirely:
|
|
8
8
|
/// every argument is decoded individually by its static type.
|
|
9
9
|
///
|
|
10
10
|
/// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
|
|
11
|
-
/// unwrapped from the JS `this` for a shared-object binding. An `async` `@JS func`
|
|
12
|
-
///
|
|
11
|
+
/// unwrapped from the JS `this` for a shared-object binding. An `async` `@JS func` binds through the
|
|
12
|
+
/// two-phase async `setProperty(_:)` overload: the closure is the synchronous decode phase (receiver
|
|
13
|
+
/// unwrap, arity guard, argument decode, all while `this` and the arguments are still valid)
|
|
14
|
+
/// and returns the async body (call and result encode), so nothing JSI-owned crosses the asynchronous
|
|
15
|
+
/// boundary and JS gets a promise.
|
|
13
16
|
internal struct JSFunction {
|
|
14
17
|
let swiftName: String
|
|
15
18
|
let jsName: String
|
|
@@ -60,7 +63,7 @@ internal struct JSFunction {
|
|
|
60
63
|
let maximum = parameters.count
|
|
61
64
|
var lines: [String] = []
|
|
62
65
|
|
|
63
|
-
if let unwrap = receiver.unwrapStatement
|
|
66
|
+
if let unwrap = receiver.unwrapStatement {
|
|
64
67
|
lines.append(unwrap)
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -87,8 +90,31 @@ internal struct JSFunction {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
if required == maximum {
|
|
90
|
-
// No omittable trailing run: a single flat call with every argument decoded.
|
|
91
|
-
|
|
93
|
+
// No omittable trailing run: a single flat call with every argument decoded. An async
|
|
94
|
+
// function decodes any remaining slots here (still the synchronous phase) and returns its
|
|
95
|
+
// body; a sync one calls and encodes directly.
|
|
96
|
+
if isAsync {
|
|
97
|
+
for index in required..<maximum {
|
|
98
|
+
lines.append(decodeStatement(at: index))
|
|
99
|
+
}
|
|
100
|
+
lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: maximum))
|
|
101
|
+
} else {
|
|
102
|
+
lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
|
|
103
|
+
}
|
|
104
|
+
} else if isAsync {
|
|
105
|
+
// One body per accepted arity, branching on `arguments.count`. A branch decodes its trailing
|
|
106
|
+
// slots synchronously and returns the async body for that call shape, so each `return` ends
|
|
107
|
+
// the decode phase for its arity.
|
|
108
|
+
lines.append("switch arguments.count {")
|
|
109
|
+
for arity in required...maximum {
|
|
110
|
+
let label = arity == maximum ? "default:" : "case \(arity):"
|
|
111
|
+
lines.append(label)
|
|
112
|
+
for index in required..<arity {
|
|
113
|
+
lines.append(" " + decodeStatement(at: index))
|
|
114
|
+
}
|
|
115
|
+
lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: arity).map { " " + $0 })
|
|
116
|
+
}
|
|
117
|
+
lines.append("}")
|
|
92
118
|
} else {
|
|
93
119
|
// One call shape per accepted arity, branching on `arguments.count`. A branch decodes a
|
|
94
120
|
// trailing slot before the call, so this is a `switch` statement (not an expression): a
|
|
@@ -116,6 +142,24 @@ internal struct JSFunction {
|
|
|
116
142
|
.joined(separator: "\n")
|
|
117
143
|
}
|
|
118
144
|
|
|
145
|
+
/// The `return { … }` statement closing an async binding's decode phase: the returned closure is
|
|
146
|
+
/// the function's async body (`AsyncFunctionBody`), awaiting the call and encoding the result. It
|
|
147
|
+
/// captures only what the call needs — the decoded `arg<i>` locals, the receiver, and `runtime`
|
|
148
|
+
/// for the encode — never `this` or the arguments buffer, which are only valid for the duration
|
|
149
|
+
/// of the host call.
|
|
150
|
+
private func asyncBodyLines(receiver: Receiver, arity: Int) -> [String] {
|
|
151
|
+
var lines: [String] = ["return {"]
|
|
152
|
+
if returnType != nil {
|
|
153
|
+
lines.append(" let result = \(callExpression(receiver: receiver, arity: arity))")
|
|
154
|
+
lines.append(contentsOf: encodeResultLines().map { " " + $0 })
|
|
155
|
+
} else {
|
|
156
|
+
lines.append(" \(callExpression(receiver: receiver, arity: arity))")
|
|
157
|
+
lines.append(" return .undefined")
|
|
158
|
+
}
|
|
159
|
+
lines.append("}")
|
|
160
|
+
return lines
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
/// `let arg<index> = …` decoding the slot at `index` by its static type through
|
|
120
164
|
/// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
|
|
121
165
|
/// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
|
|
@@ -177,40 +221,51 @@ internal struct JSFunction {
|
|
|
177
221
|
/// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
|
|
178
222
|
/// catchable error is the right behavior, and matches how non-primitive integers already encode. A
|
|
179
223
|
/// no-return function returns `.undefined` instead.
|
|
224
|
+
///
|
|
225
|
+
/// For an `async` function the encode must run on the JS thread. An `async` body may suspend and
|
|
226
|
+
/// resume on an arbitrary cooperative-pool thread, and encoding a heap-allocated JS value (a string,
|
|
227
|
+
/// object, array, or typed array) off the JS thread races the engine's garbage collector and
|
|
228
|
+
/// corrupts the heap. `runtime.execute` hops the encode onto the JS thread, running inline when it is
|
|
229
|
+
/// already there (the common case, where the body never truly suspended), so synchronous functions —
|
|
230
|
+
/// which always run on the JS thread — keep encoding directly without the wrapper.
|
|
180
231
|
private func encodeResultLines() -> [String] {
|
|
181
232
|
guard let returnType else {
|
|
182
233
|
return ["return .undefined"]
|
|
183
234
|
}
|
|
184
|
-
|
|
235
|
+
let encode = "try \(expressionType(returnType)).encode(result, in: runtime)"
|
|
236
|
+
if isAsync {
|
|
237
|
+
return [
|
|
238
|
+
"return try await runtime.execute {",
|
|
239
|
+
" return \(encode)",
|
|
240
|
+
"}",
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
return ["return \(encode)"]
|
|
185
244
|
}
|
|
186
245
|
|
|
187
246
|
/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
|
|
188
247
|
/// body is inlined directly into the closure passed to the closure-taking `setProperty` overload
|
|
189
248
|
/// (which creates the host function under the hood) — no separate named binding. For an `async`
|
|
190
|
-
/// function the
|
|
191
|
-
/// receives a promise).
|
|
249
|
+
/// function the closure is the synchronous decode phase and returns the async body, which selects
|
|
250
|
+
/// the async `setProperty` overload (so JS receives a promise).
|
|
192
251
|
///
|
|
193
|
-
///
|
|
252
|
+
/// A module captures its `self` **strong** —
|
|
194
253
|
/// the host-function closure is what keeps the native callable alive for as long as JS can invoke
|
|
195
254
|
/// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
|
|
196
255
|
/// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
|
|
197
|
-
func decorateStatements(receiver: Receiver) -> String {
|
|
198
|
-
//
|
|
199
|
-
// `
|
|
200
|
-
//
|
|
201
|
-
// `this`; a shared object unwraps it (still
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
256
|
+
func decorateStatements(object: String, receiver: Receiver) -> String {
|
|
257
|
+
// Every `@JS` binding hands `this` in as a borrowed `JavaScriptUnownedValue` instead of
|
|
258
|
+
// allocating an owning `JavaScriptValue` and forming its `weak`-runtime reference on every call,
|
|
259
|
+
// and consumes the arguments buffer — sync and async closures share one parameter shape and
|
|
260
|
+
// differ only in what they return. A module ignores `this`; a shared object unwraps it (still
|
|
261
|
+
// borrowed) — an async binding does so in its synchronous decode phase, before the borrow ends.
|
|
262
|
+
// The parameter list is parenthesized and fully typed, since Swift rejects a type annotation on
|
|
263
|
+
// a shorthand `{ [capture] name, name in }` parameter; for a sync binding the explicit
|
|
264
|
+
// `JavaScriptUnownedValue` also selects the (otherwise `@_disfavoredOverload`) unowned-`this`
|
|
265
|
+
// overload.
|
|
207
266
|
let captures = receiver.captureClause
|
|
208
|
-
let parameters =
|
|
209
|
-
isAsync
|
|
210
|
-
? "this, arguments"
|
|
211
|
-
: "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
267
|
+
let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
212
268
|
|
|
213
|
-
let object = receiver.decoratedObject
|
|
214
269
|
return """
|
|
215
270
|
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
216
271
|
\(bodyStatements(receiver: receiver, indent: " "))
|
|
@@ -221,9 +276,9 @@ internal struct JSFunction {
|
|
|
221
276
|
|
|
222
277
|
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
223
278
|
/// `Property(...)` DSL entry, the enclosing macro synthesizes a get/set accessor into the JS object
|
|
224
|
-
/// inside its decorator (`_decorateModule` / `_decorateSharedObject`): it builds a descriptor object
|
|
279
|
+
/// inside its decorator (`_decorateModule(object:)` / `_decorateSharedObject(prototype:)`): it builds a descriptor object
|
|
225
280
|
/// (`enumerable` + `get`, and `set` when the property is settable) and installs it with
|
|
226
|
-
/// `object.defineProperty(name, descriptor:)
|
|
281
|
+
/// `object.defineProperty(name, descriptor:)`.
|
|
227
282
|
/// The `get`/`set` host functions are installed the same way `@JS func`s are — the closure-taking
|
|
228
283
|
/// `setProperty(_:)` overload, with the read/write body inlined into the closure.
|
|
229
284
|
///
|
|
@@ -248,14 +303,13 @@ internal struct JSProperty {
|
|
|
248
303
|
/// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
|
|
249
304
|
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
250
305
|
/// instance. Getter and setter are gated independently.
|
|
251
|
-
func decorateStatements(receiver: Receiver) -> String {
|
|
306
|
+
func decorateStatements(object: String, receiver: Receiver) -> String {
|
|
252
307
|
let descriptorName = "\(swiftName)Descriptor"
|
|
253
308
|
let callee = receiver.callee
|
|
254
|
-
let object = receiver.decoratedObject
|
|
255
309
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
256
310
|
// reads `self` directly. The unwrap leads each accessor body. Property accessors are synchronous, so
|
|
257
311
|
// they take the borrowed unowned `this`.
|
|
258
|
-
let unwrap = receiver.unwrapStatement
|
|
312
|
+
let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
|
|
259
313
|
var lines: [String] = []
|
|
260
314
|
|
|
261
315
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
@@ -319,24 +373,30 @@ internal struct JSProperty {
|
|
|
319
373
|
}
|
|
320
374
|
}
|
|
321
375
|
|
|
322
|
-
/// The body shared by
|
|
323
|
-
/// and every `@JS var` via a `defineProperty` accessor, joined for the function body.
|
|
324
|
-
///
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
376
|
+
/// The body shared by every decorator phase: every `@JS func` bound via an inlined `setProperty`
|
|
377
|
+
/// closure and every `@JS var` via a `defineProperty` accessor, joined for the function body. `object`
|
|
378
|
+
/// is the local the bindings decorate (matching the entry point's argument label); `receiver` selects
|
|
379
|
+
/// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
|
|
380
|
+
/// metatype for a static member).
|
|
381
|
+
private func decorateBody(
|
|
382
|
+
functions: [JSFunction], properties: [JSProperty], object: String, receiver: Receiver
|
|
383
|
+
) -> String {
|
|
384
|
+
let functionBody = functions.map { $0.decorateStatements(object: object, receiver: receiver) }
|
|
385
|
+
let propertyBody = properties.map { $0.decorateStatements(object: object, receiver: receiver) }
|
|
328
386
|
return (functionBody + propertyBody).joined(separator: "\n")
|
|
329
387
|
}
|
|
330
388
|
|
|
331
|
-
/// The
|
|
332
|
-
///
|
|
333
|
-
///
|
|
334
|
-
///
|
|
335
|
-
///
|
|
336
|
-
///
|
|
337
|
-
///
|
|
389
|
+
/// The module decorator: a `_decorateModule(object:)` that binds every `@JS func` (via an inlined
|
|
390
|
+
/// `setProperty` closure) and every `@JS var` (via a `defineProperty` accessor) into the module's own
|
|
391
|
+
/// JS object. Core supplies the object; the bindings call into the module `self`. It's an *instance*
|
|
392
|
+
/// method (a module is a singleton, satisfying the `AnyModule` requirement of the same name), with a
|
|
393
|
+
/// `borrowing` object parameter (it mutates through the reference without reassigning or taking
|
|
394
|
+
/// ownership). Only emitted when there's at least
|
|
395
|
+
/// one member to bind. Uses the shared body generation with the module `object:` phase and `self`
|
|
396
|
+
/// receiver; the shared-object counterpart is `buildDecorateSharedObjectPhase`.
|
|
338
397
|
internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
|
|
339
|
-
let body = decorateBody(
|
|
398
|
+
let body = decorateBody(
|
|
399
|
+
functions: functions, properties: properties, object: Phase.object.rawValue, receiver: .module)
|
|
340
400
|
return """
|
|
341
401
|
@JavaScriptActor
|
|
342
402
|
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
@@ -345,22 +405,28 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
345
405
|
"""
|
|
346
406
|
}
|
|
347
407
|
|
|
348
|
-
///
|
|
349
|
-
///
|
|
350
|
-
///
|
|
351
|
-
///
|
|
352
|
-
///
|
|
353
|
-
///
|
|
354
|
-
///
|
|
355
|
-
///
|
|
356
|
-
/// `@JS
|
|
357
|
-
|
|
358
|
-
|
|
408
|
+
/// A shared-object decorator phase, a label overload of `_decorateSharedObject` overriding the matching
|
|
409
|
+
/// `open class func` on the `SharedObject` base so core can dispatch through the concrete type's
|
|
410
|
+
/// metatype. The `prototype:` overload binds the type's instance `@JS func`/`var` members onto the class
|
|
411
|
+
/// prototype (the original hook, unchanged); the `constructor:` overload binds the `static`/`class` ones
|
|
412
|
+
/// onto the constructor function itself (a new, additive overload, so introducing it isn't a breaking
|
|
413
|
+
/// core change). Both are *static* (a shared object has no singleton `self`). An instance binding
|
|
414
|
+
/// recovers its typed receiver from the JS `this` per call (`SharedObject.native(from:as:)`); a static
|
|
415
|
+
/// binding calls the Swift member on the type and ignores `this` (which, on the static side, is the
|
|
416
|
+
/// constructor). The constructor *object* the static members decorate is distinct from the `@JS init`,
|
|
417
|
+
/// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
|
|
418
|
+
/// is emitted only when the type has a member for it.
|
|
419
|
+
internal func buildDecorateSharedObjectPhase(
|
|
420
|
+
phase: Phase, functions: [JSFunction], properties: [JSProperty], typeName: String
|
|
359
421
|
) -> DeclSyntax {
|
|
360
|
-
let
|
|
422
|
+
let receiver: Receiver = phase == .constructor
|
|
423
|
+
? .staticMember(typeName: typeName)
|
|
424
|
+
: .sharedObject(typeName: typeName)
|
|
425
|
+
let body = decorateBody(
|
|
426
|
+
functions: functions, properties: properties, object: phase.rawValue, receiver: receiver)
|
|
361
427
|
return """
|
|
362
428
|
@JavaScriptActor
|
|
363
|
-
public override class func _decorateSharedObject(
|
|
429
|
+
public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
364
430
|
\(raw: body)
|
|
365
431
|
}
|
|
366
432
|
"""
|
|
@@ -98,14 +98,6 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
98
98
|
return nil
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/// True when the modifiers make the member type-level (`static` or `class`), so its assertion peer
|
|
102
|
-
/// must be emitted in the same metatype context rather than as an instance member.
|
|
103
|
-
private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
104
|
-
return modifiers.contains {
|
|
105
|
-
$0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
101
|
/// True when a return clause is written as `Void` / `()` — nothing crosses the boundary, so it needs
|
|
110
102
|
/// no conformance assertion. (A missing return clause never reaches here: `returnClause` is `nil`.)
|
|
111
103
|
private func isVoidType(_ type: TypeSyntax) -> Bool {
|
|
@@ -43,6 +43,15 @@ internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
|
43
43
|
return false
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/// True when the modifiers make the member type-level (`static` or `class`). A shared object routes
|
|
47
|
+
/// such members to its constructor (JS-static) and instance members to its prototype; the `@JS`
|
|
48
|
+
/// conformance assertion also uses this to emit its peer in the matching metatype context.
|
|
49
|
+
internal func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
50
|
+
return modifiers.contains {
|
|
51
|
+
$0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
/// True if a trailing occurrence of this parameter may be omitted by the JS caller: it either has a
|
|
47
56
|
/// default value (Swift applies it) or is an optional type (an absent slot becomes `nil`). The arity
|
|
48
57
|
/// range and the per-arity call branches are derived from this.
|
|
@@ -1,62 +1,79 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
/// Where a directly-bound closure gets the Swift value it calls into.
|
|
4
|
-
///
|
|
5
|
-
///
|
|
3
|
+
/// Where a directly-bound closure gets the Swift value it calls into. This is one of the two
|
|
4
|
+
/// orthogonal axes of a binding; the other is the `Phase` (which JS object the member is installed
|
|
5
|
+
/// on). The two are independent: choosing the prototype phase does not by itself decide whether the
|
|
6
|
+
/// receiver is `self`, `_self`, or the metatype.
|
|
7
|
+
///
|
|
8
|
+
/// - A module is a singleton, so its bindings call `self` and ignore the JS `this`.
|
|
9
|
+
/// - A shared-object *instance* member has a distinct native instance per JS object, so it recovers
|
|
10
|
+
/// the typed receiver from `this`.
|
|
11
|
+
/// - A `static`/`class` member has no instance at all; it calls the Swift member on the metatype
|
|
12
|
+
/// (`Cache.open(…)`) and ignores `this` (which, on the static side, is the constructor).
|
|
6
13
|
internal enum Receiver {
|
|
7
14
|
/// The module singleton; the closure captures `self` strong.
|
|
8
15
|
case module
|
|
9
16
|
/// A shared object of the given concrete type; the closure captures nothing and recovers the receiver
|
|
10
17
|
/// from `this` per call.
|
|
11
18
|
case sharedObject(typeName: String)
|
|
19
|
+
/// A `static`/`class` member of the given concrete type; the closure captures nothing and calls the
|
|
20
|
+
/// Swift member on the type itself, ignoring `this`.
|
|
21
|
+
case staticMember(typeName: String)
|
|
12
22
|
|
|
13
23
|
/// The expression the body calls members on: `self` for a module, `_self` (bound by `unwrapStatement`)
|
|
14
|
-
/// for a shared
|
|
24
|
+
/// for a shared-object instance, the type name for a static member. The leading underscore on `_self`
|
|
25
|
+
/// avoids colliding with a user member like `var owner`.
|
|
15
26
|
var callee: String {
|
|
16
27
|
switch self {
|
|
17
28
|
case .module:
|
|
18
29
|
return "self"
|
|
19
30
|
case .sharedObject:
|
|
20
31
|
return "_self"
|
|
32
|
+
case .staticMember(let typeName):
|
|
33
|
+
return typeName
|
|
21
34
|
}
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
/// The
|
|
25
|
-
///
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case .module:
|
|
29
|
-
return "object"
|
|
30
|
-
case .sharedObject:
|
|
31
|
-
return "prototype"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/// The leading body line binding the receiver, or `nil` for a module (it reads `self` directly). For a
|
|
36
|
-
/// shared object, `native(from:as:)` recovers the typed instance from `this`, throwing on a foreign
|
|
37
|
-
/// object or a type mismatch.
|
|
37
|
+
/// The leading body line binding the receiver, or `nil` when nothing needs to be unwrapped (a module
|
|
38
|
+
/// reads `self` directly; a static member calls the type directly). For a shared-object instance,
|
|
39
|
+
/// `native(from:as:)` recovers the typed instance from `this`, throwing on a foreign object or a type
|
|
40
|
+
/// mismatch.
|
|
38
41
|
///
|
|
39
|
-
/// The `this` object comes from the borrowed `JavaScriptUnownedValue`
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
|
|
42
|
+
/// The `this` object always comes from the borrowed `JavaScriptUnownedValue` (`asObject(in:)`): an
|
|
43
|
+
/// async binding unwraps in its synchronous decode phase, before the borrowed value's lifetime ends,
|
|
44
|
+
/// and only the recovered native instance crosses into the async body.
|
|
45
|
+
var unwrapStatement: String? {
|
|
43
46
|
switch self {
|
|
44
|
-
case .module:
|
|
47
|
+
case .module, .staticMember:
|
|
45
48
|
return nil
|
|
46
49
|
case .sharedObject(let typeName):
|
|
47
|
-
let
|
|
48
|
-
return "let _self = try SharedObject.native(from: \(thisObject), as: \(typeName).self)"
|
|
50
|
+
return "let _self = try SharedObject.native(from: this.asObject(in: runtime), as: \(typeName).self)"
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/// The capture-clause fragment (with a trailing space, or empty when nothing is captured). A module
|
|
53
|
-
/// captures `self` strong; a shared
|
|
55
|
+
/// captures `self` strong; a shared-object instance and a static member capture nothing.
|
|
54
56
|
var captureClause: String {
|
|
55
57
|
switch self {
|
|
56
58
|
case .module:
|
|
57
59
|
return "[self] "
|
|
58
|
-
case .sharedObject:
|
|
60
|
+
case .sharedObject, .staticMember:
|
|
59
61
|
return ""
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
|
|
66
|
+
/// Which JS object a set of bindings is installed on: the second orthogonal axis alongside `Receiver`.
|
|
67
|
+
/// It selects the decorator entry point's argument label and the local name the body binds members onto,
|
|
68
|
+
/// mirroring JS class semantics (a class has a constructor function whose `.prototype` carries instance
|
|
69
|
+
/// members).
|
|
70
|
+
/// The raw value is the argument label of the decorator entry point, which is also the local name the
|
|
71
|
+
/// body binds members onto.
|
|
72
|
+
internal enum Phase: String {
|
|
73
|
+
/// A concrete JS object: a singleton's own object (the module). Instance method; receiver `self`.
|
|
74
|
+
case object
|
|
75
|
+
/// The class constructor's `prototype`, carrying per-instance members. Static; receiver `_self`.
|
|
76
|
+
case prototype
|
|
77
|
+
/// The class constructor function itself, carrying `static`/`class` members. Static; receiver the type.
|
|
78
|
+
case constructor
|
|
79
|
+
}
|
|
@@ -42,13 +42,20 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
42
42
|
let jsName = jsNameArgument(of: node) ?? typeName
|
|
43
43
|
|
|
44
44
|
// `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
|
|
45
|
-
// the synthesized `_decorateSharedObject` / `
|
|
46
|
-
// `Function(...)` / `Property(...)` / `Constructor { … }` DSL entry, so
|
|
47
|
-
// instead of appended to the `Class` block. The block keeps only non-`@JS`
|
|
48
|
-
// collected today), so it's empty when every member is `@JS`.
|
|
45
|
+
// the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`
|
|
46
|
+
// rather than described with a `Function(...)` / `Property(...)` / `Constructor { … }` DSL entry, so
|
|
47
|
+
// they're collected here instead of appended to the `Class` block. The block keeps only non-`@JS`
|
|
48
|
+
// definitions (none are collected today), so it's empty when every member is `@JS`.
|
|
49
|
+
//
|
|
50
|
+
// Members split by the `static`/`class` modifier onto two different JS objects: instance members
|
|
51
|
+
// decorate the prototype (receiver recovered from JS `this`), static members decorate the
|
|
52
|
+
// constructor (called on the metatype). A JS instance and static member may share a name without
|
|
53
|
+
// colliding: they live on different objects.
|
|
49
54
|
let entries: [String] = []
|
|
50
|
-
var
|
|
51
|
-
var
|
|
55
|
+
var instanceFunctions: [JSFunction] = []
|
|
56
|
+
var instanceProperties: [JSProperty] = []
|
|
57
|
+
var staticFunctions: [JSFunction] = []
|
|
58
|
+
var staticProperties: [JSProperty] = []
|
|
52
59
|
var constructor: JSConstructor?
|
|
53
60
|
|
|
54
61
|
for member in classDecl.memberBlock.members {
|
|
@@ -66,13 +73,23 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
66
73
|
|
|
67
74
|
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
68
75
|
let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
|
|
69
|
-
|
|
76
|
+
let function = JSFunction(funcDecl: funcDecl, attribute: attribute)
|
|
77
|
+
if isTypeLevel(funcDecl.modifiers) {
|
|
78
|
+
staticFunctions.append(function)
|
|
79
|
+
} else {
|
|
80
|
+
instanceFunctions.append(function)
|
|
81
|
+
}
|
|
70
82
|
continue
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
74
86
|
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
75
|
-
|
|
87
|
+
let collected = collectProperties(varDecl: varDecl, attribute: attribute)
|
|
88
|
+
if isTypeLevel(varDecl.modifiers) {
|
|
89
|
+
staticProperties.append(contentsOf: collected)
|
|
90
|
+
} else {
|
|
91
|
+
instanceProperties.append(contentsOf: collected)
|
|
92
|
+
}
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
95
|
|
|
@@ -89,13 +106,22 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
89
106
|
"""
|
|
90
107
|
]
|
|
91
108
|
|
|
92
|
-
// Direct JSI binding
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
109
|
+
// Direct JSI binding, split by which JS object each member decorates:
|
|
110
|
+
// `_decorateSharedObject(prototype:)` binds instance `@JS func`/`var`s (unwrapping the per-call receiver from
|
|
111
|
+
// `this`); `_decorateSharedObject(constructor:)` binds `static`/`class` ones (called on the metatype); and
|
|
112
|
+
// `_constructSharedObject` builds an instance from the `@JS init` arguments. Each is emitted only
|
|
113
|
+
// when it has something to bind.
|
|
114
|
+
if !instanceFunctions.isEmpty || !instanceProperties.isEmpty {
|
|
115
|
+
emitted.append(
|
|
116
|
+
buildDecorateSharedObjectPhase(
|
|
117
|
+
phase: .prototype, functions: instanceFunctions, properties: instanceProperties,
|
|
118
|
+
typeName: typeName))
|
|
119
|
+
}
|
|
120
|
+
if !staticFunctions.isEmpty || !staticProperties.isEmpty {
|
|
97
121
|
emitted.append(
|
|
98
|
-
|
|
122
|
+
buildDecorateSharedObjectPhase(
|
|
123
|
+
phase: .constructor, functions: staticFunctions, properties: staticProperties,
|
|
124
|
+
typeName: typeName))
|
|
99
125
|
}
|
|
100
126
|
if let constructor {
|
|
101
127
|
emitted.append(constructor.buildConstructor(typeName: typeName))
|