Skip to content
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

Support implementing Symbol-based EcmaScript protocols like Iterable #28

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

justjake
Copy link

@justjake justjake commented Jul 8, 2024

This adds a set of features that allow Swift developers to implement protocols using Symbols, like Iterable:

  • Add helpers to NodeSymbol to access well-know symbols like Symbol.iterator, or the global symbol registry like Symbol.for("user.land.protocol").
  • @NodeName(replacementName) renames members exposed with @NodeClass + @NodeProperty or @NodeMethod. To implement a symbol-based protocol, pass the symbol as the replacementName.
  • Implement NodeIterator class to support the Iterable protocol. An example iterable is included in an integration test.

Individual commits have some more details.

Discussion points:

  • Should the renaming functionality @NodeName be better as a parameter for the existing @NodeMethod / @NodeProperty macros? It was easier to implement as a new macro for me because I'm new to Swift.
  • I avoided using macros to implement NodeIterator since I didn't see other uses within the library. Should I convert that class to a @NodeClass macro? Since there's only one method exposed, it's not much difference in code size in this instance.
  • The NodeIterator class is used for supporting Swift developers writing Iterable classes to Node, not for consuming Node iterators in Swift, although that could also be useful as well. Should this implementation do both? Or is this kind of thing out of scope for the node-swift library?
  • Code style: I tried to follow four-space indents as best I could. Is there an auto-formatting setup you use? My editor tried its best to really mess things up, and it would be easier to contribute if there was some swift-format or swiftformat tool set up.

This is most-useful to use NodeSymbol names for methods to implement
well-known interfaces, but it could also be useful anywhere you want
Swift names and Node names for APIs to diverge.

Alternatively, we could add name parameters to the existing
`@NodeProperty` and `@NodeMethod` macros. This  was more
straight-forward to implement for a macro beginner.
- `NodeSymbol.wellKnown(propertyName: "name")` is equivalent to
  `Symbol['name']`. This is useful for implementing language-level
  protocols.
- `NodeSymbol.global(for: "name")` is equivalent to `Symbol.for('name')` in JS. This is useful for implementing userland protocols.

Both those static methods may throw, so there's also deferred versions
which follow the existing convention of `deferredConstructor` to
postpone building the NodeValue symbol until it's needed on the
@NodeActor thread.
Adds `NodeIterator` class and extension to NodeValueConvertible Sequence to support implementing the EcmaScript iterable protocol from Swift.

Adds an iterable protocol integration test to demonstrate how the new
features on this branch work together.

Discussion point: none of the other internal classes seem to use macros,
so I implemented NodeClass manually for NodeIterator. Is it okay to use
the macros here?
The dynamic call behavior makes it easy to call random fictional APIs in
Swift code on NodeValue instances. I hit this error several times when
trying to iron out NodeIterator stuff. I was mistakenly doing this:

```swift
try nodeObj.set("prop", newValue)
```

which is legal from the compiler's perspective, but attempts to call a
non-existant function `set` on `nodeObj`. The correct Swift syntax is:

```swift
try nodeObj["prop"].set(to: newValue)
```

By including the DynamicProperty.key in the error message, it's much
easier to find the Swift code responsible for the error throw in
Javascript.
For me, the test harness works fine without calling `builder.clean()`
and significantly improves my iteration speed to not have to wait for a
full rebuild whenever I run a single test suite.

Running all tests should still clean by default, but in both cases this
can be configured when running like `CLEAN=1 node index.js suite Test`
or `CLEAN=0 node index.js all`.
Comment on lines +61 to +77
final class SomeIterable: NodeClass {
typealias Element = String

static let properties: NodeClassPropertyList = [
NodeSymbol.iterator: NodeMethod(nodeIterator),
]

static let construct = NodeConstructor(SomeIterable.init(_:))
init(_ args: NodeArguments) throws { }

private let values: [String] = ["one", "two", "three"]

func nodeIterator() throws -> NodeIterator {
values.nodeIterator()
}

}
Copy link
Author

Choose a reason for hiding this comment

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

Should this code prefer the macro?

Comment on lines +196 to +198
if let dynamicProperty = self as? NodeObject.DynamicProperty {
return "\(receiver).\(dynamicProperty.key) is \(actual)"
}
Copy link
Author

Choose a reason for hiding this comment

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

Perhaps it would be better for DynamicProperty to implement CustomStringRepresentable in like "(obj).(key)", so that stringifying a DynamicProperty anywhere prints the full path 🤔

@kabiroberai
Copy link
Owner

hey @justjake, thanks for the PR! Just wanted to let you know that I've given the code a first pass and these look like great changes. I'm a tad busy right now but I'll leave feedback asap, hopefully over the coming week. In the meantime, do you mind splitting up the @NodeName macro into a separate PR? I think that's a slam dunk addition as-is (modulo some nits) and I'd be happy to merge that separately.

NodeIterator also seems very useful but it warrants some thought about whether we should introduce additional layering into this package. So far NodeAPI has largely aimed to replicate the C Node-API surface 1:1. There's definitely room for ergonomics on top of that, but I wonder if we should keep those in a separate module within the package. That could be a good place to move stuff from Sugar.swift et al too.

@kabiroberai kabiroberai self-requested a review July 14, 2024 20:39
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.

2 participants