Skip to content

Scala.js implementation report #76

@sjrd

Description

@sjrd

Since I had quite a few hours in the train on the way back from Munich, I implemented custom descriptors in the Scala.js emitter. In total I think it took me 2 full working days to get everything done and polished.

The implementation PR is there: scala-js/scala-js#5255
We won't merge it before there is a Node.js 25.2.0, and also our second main maintainer is currently on vacation, so it will stay there for a few weeks at least.

That said, some interesting bits. I have no performance evaluation at the moment, only expressivity/semantics reports.

It does allow us to implement @JSExport

In Scala.js, one can annotate methods and properties of Scala classes with @JSExport. This is supposed to let JavaScript call those methods, assuming it holds an instance of said class. This was the only bit of Scala.js semantics that we could not implement in Wasm before.

I'm happy to report that Custom Descriptors does fill that gap. methodconfigs and parentidx prototype chaining give us what we need. We can now run the full Scala.js test suite. We don't need exceptions to skip the tests about @JSExport.

null value for the constructors param of configureAll?

We do not use constructorconfig. That's not a problem against the spec, obviously. We have other mechanisms for "top-level" exports in the JS interop layer that are appropriate for Scala.js semantics.

What was a bit surprising is that the 4th parameter of configureAll is therefore useless to us. Its type is externref, so I initially thought I could give it a ref.null. It turns out this yields an "Invalid cast" in V8. Is that expected? Should we be able to pass ref.null noextern if we have no constructorconfig?

Methods with ...rest parameters

I mentioned it before, but to be complete: it's a bummer for us that we can't define methodconfigs that have ...rest parameters. We can use configureAll for all other @JSExports. For @JSExport methods with a ...rest param, we have to fall back to JS glue code.

You can find here our call to configureAll, followed by the additional glue code to deal with the ...rest params:
https://github.com/scala-js/scala-js/pull/5255/files#diff-2755f7122e3b32c0e33f9b9ca3af52496f76ade5f938140654de61853cedd900R330
Note that we need a lot of JS glue code for many other parts of our compiler, so this is really not an issue. It's just a bit weird sitting there, so I'm pointing it out anyway.

Dynamic descriptor allocation

At run-time, Scala shares the peculiarities of Java arrays. In particular, arrays of object types retain their component type in their metadata. Moreover, because of APIs like jl.reflect.Array.newInstance, the set of distinct array types we will need is only dynamically learned at run-time. In Scala.js-on-Wasm, we do this by using a single struct type for ObjectArray, but we dynamically create distinct instances of its vtable type. So we end up with one vtable for jl.Object[], one vtable for jl.String[], one other for jl.String[][], etc.

I expected it, but I was still happy that this scheme worked out-of-the-box with descriptors. We can dynamically create distinct descriptors of ObjectArray for every array type we discover at run-time. It's very nice that we are actually able to do that!

java.lang.Throwable and JS' Error

In Scala.js-on-JS, we rewire our Throwable class so that it truly extends Error. This has several advantages. The most notable one is that debuggers understand that they should store stack trace information in them. When we throw an instance of jl.Throwable, it benefits from all the debugging features you would expect.

With custom descriptors, we are able to do part of that rewiring. We make the prototype we attach to jl.Throwable inherit from Error.prototype. We do this with configureAll, but it's a bit funny. We have to pass in Error.prototype as one of the prototypes we give to configureAll, only to get a parentidx for it. But we do not configure anything to it (constructorconfigs, methodconfigs are empty and parentidx is -1). I'm not sure if that has any adverse effect on Error.prototype on a global scale?

Anyway, doing that, Error.prototype appears on the prototype chain of our Throwables, which is nice for things like x instanceof Error.

What we're still missing is that our Throwables are still not true error objects. In spec terms, they don't have an [[ErrorData]] attribute. That means the debuggers still don't store anything in them for debugging purposes.

I feel like this is still a gap we should try to fill, but I'm not sure whether Custom Descriptors is the right place for that?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions