Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Introduce NewLambda to synthesize instances of SAM types.#5003

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
sjrd merged 2 commits intoscala-js:mainfromsjrd:typed-closures
Mar 16, 2025

Conversation

sjrd
Copy link
Member

@sjrdsjrd commentedJul 1, 2024
edited
Loading

At this moment, this is a more a Request For Comments than a real PR. It is built on#4988 since the motivation is entirely about making the Wasm output fast instead of disastrously slow when Scala functions are involved.

Was:Introduce typed closures.
Now the focus in more onNewLambda than typedClosure nodes.


TheNewLambda node creates an instance of an anonymous class from aDescriptor and a closurefun. TheDescriptor specifies the shape of the anonymous class: a super class, a list of interfaces to implement, and the name of a single non-constructor method to provide. The body of the method calls thefun closure.

At link time, the Analyzer and BaseLinker synthesize a unique such anonymous class perDescriptor. In practice, all the lambdas for a given target type share a commonDescriptor. This is notably the case for all the Scala functions of arity N.

NewLambda replaces the need for specialAnonFunctionN classes in the library. Instead, classes of the right shape are synthesized at link-time.

The scheme can also be used for mostLambdaMetaFactory-style lambdas, although ourNewLambda does not support bridge generation. In the common case where no bridges are necessary, we now also generate aNewLambda. This generalizes the code size optimization of having only one class perDescriptor to non-Scala functions.

In order to truly support LMF-style lambdas, the closurefun must take parameters that match the (erased) type in their superinterface. Previously, for ScalaFunctionN, we knew by construction that the parameters and result types were alwaysany, and so JSClosures were good enough. Now, we need closures that can accept different types. This is where typed closures come into play (see below).


When bridges are required, we still generate a custom class from the compiler backend. In that case, we statically inline the closure body in the produced SAM implementation.

We have to do this not to expose typed closures across method calls. Moreover, we need the better static types for the parameters to be able to inline the closures without too much hassle. So this changehas to be done in lockstep with the rest of this commit.


A typed closure is aClosure that does not have any semantics for JS interop. This is stronger thanChar, which is "merely" opaque to JS. AChar can still be passed to JS and has a meaningfultoString(). A typed closurecannot be passed to JS in any way. That is enforced by making their typenot a subtype ofany (like record types).

Since a typed closure has no JS interop semantics, it is free to strongly, statically type its parameters and result type.

Additionally, we can freely choose its representation in the best possible way for the given target. On JS, that remains an arrow function. On Wasm, however, we represent it as a pair of(capture data pointer, function pointer). This allows to compile them in an efficient way that does not require going through a JS bridge closure. The latter has been shown to have a devastating impact on performance when a Scala function is used in a tight loop.

The type of a typed closure is aClosureType. It records its parameter types and its result type. Closure types are non-variant: they are only subtypes of themselves. As mentioned, they are not subtypes ofany. They are however subtypes ofvoid and supertypes ofnothing. Unfortunately, they must also be nullable to have a default value, so they have nullable and non-nullable alternatives.

To call a typed closure, we introduce a dedicated application nodeApplyTypedClosure. IR checking ensures that actual arguments match the expected parameter types. The result type is directly used as the type of the application.

There are no changes to the source language. In particular, there is no way to express typed closures or their types at the user level. They are only used forNewLambda nodes.

In fact, typed closures andApplyTypedClosures are not first-class at the IR level. Before desugaring, typed closures are only allowed as direct children ofNewLambda nodes. Desugaring transformsNewLambda nodes intoNews of the synthesized anonymous classes. At that point, the two typed closure nodes become first-class expression trees.


For Scala functions, these changes have no real impact on the JS output (only marginal naming differences). On Wasm, however, they make Scala functions much, much faster. Before, a Scala function in a tight loop would cause a Wasm implementation to be, in the worst measured case, 20x slower than on JS. After these changes, similar benchmarks become significantly faster on Wasm than on JS.

He-Pin reacted with thumbs up emojiHe-Pin reacted with heart emoji
@sjrd
Copy link
MemberAuthor

sjrd commentedJul 1, 2024

@gzm0 This is based on my experiments with trying to optimize the Wasm output. For everything else, I managed to make the Wasm output on average faster than Scala.js without touching the IR, or only in a way that also benefits JS (see#4998). For Scala functions, however, they remained desperately slow no matter what I tried. With these changes to the IR, overall Wasm reaches 0.85% the geomean run time wrt. JS -- so, 15% faster on average.

When you get the chance, I'd like your broad opinion on this. Is this something we could commit to at this stage? Or should we only venture this far if and when Wasm gets more mature/battle-tested? There's obviously a catch-22 here: given the performance results, Scala.js-on-Wasm might not be production-viable without this change; but without production experience we might not implement it. That doesn't mean we couldn't wait at least a full release cycle and reports of Wasm beingcorrect, even if it is slow.

@sjrdsjrdforce-pushed thetyped-closures branch 3 times, most recently fromfd2b383 toa98c757CompareJuly 2, 2024 08:42
@gzm0
Copy link
Contributor

gzm0 commentedJul 6, 2024

I have been thinking about this, and I think what is happening is actually the other way around: the Scala.js compiler is over optimizing for the JS backend.

Closures have two separate uses ATM IIUC:

  • JS functions. This is not going away.
  • An optimized representation for Scala lambdas. This is what this PR is about.

The compiler is making the decision to perform the second optimization. However, it seems to only be an optimization that works well for the JS backend. This makes sense, since it has native support for lambdas, whereas WASM doesn't.

How would the world look, if we instead moved the Scala lambda optimization from the compiler to the JS backend?

IIUC to decide whether we can emit a Scala X class as a closure:

  • We need to make the decision globally
  • X may only have a single instance method.
  • X may only have a single instantiation site (and a fortiori only a single constructor in use).
  • X may not have subclasses.
  • X may not have runtime data.
  • X may not have instance checks.

It seems to me that this is quite similar to the inlineable init optimization. And it also seems it has the potential to allow for more optimization (n.b. post optimizer closure optimization or closure optimization for classes where only a single method is used).

WDYT?

@gzm0
Copy link
Contributor

gzm0 commentedJul 6, 2024

Ah and I forgot: IIUC if we do this, then the WASM backend shouldn't need any adjustment at all, because "typed closures" are just classes at this point (and IIUC the runtime layout would be quite similar, except for the vtable/typedata pointer overhead).

@gzm0
Copy link
Contributor

gzm0 commentedJul 6, 2024

Additional 2 things I forgot:

  • toString
  • "external" reading of fields

@sjrd
Copy link
MemberAuthor

sjrd commentedJul 7, 2024

Identifying all these conditions would be really tricky. JusttoString poses a significant issue because the default usesgetClass.getName. I don't think reverse-engineering classes this way would be effective.

However, we could go the other way: not creating a class at all, and instead have aLambdaMetaFactory-like opcode. It would take one superclass or super interface, one method to implement, and a pointer to a method that implements it (or a Typed Closure). Then the linker materializes whatever it wants to implement this concise spec. This is essentially what happens on the JVM.

One benefit is that we can then apply the best optimization also for SAM interfaces other than Scala functions (e.g.,Runnable). That might mean that the linker synthesizes a closure-carrying class per pair (superclass, method-to-implement).

In terms of code size, even for Wasm the single class carrying a Typed Closure is very beneficial. So expanding full classes is not necessarily the best strategy.

As far as I can tell, the best strategy would be a closure-carrying single class, with optimized dispatch that eagerly type-tests for that single class. So when calling aFunction1.apply, dispatch would test whether it is actually aUnique$Function1. If yes, directly call the closure it carries, otherwise, perform interface dispatch.

That is something we could do with an LMF-style opcode, which we would have a hard time reverse engineering from already expanded classes.

@sjrdsjrdforce-pushed thetyped-closures branch 2 times, most recently from95a3e59 tod7d256cCompareAugust 26, 2024 18:15
@sjrdsjrdforce-pushed thetyped-closures branch 2 times, most recently from1785a96 tof3e5d03CompareSeptember 23, 2024 08:12
@sjrdsjrdforce-pushed thetyped-closures branch 4 times, most recently from2a7e46b to58fe5f1CompareOctober 2, 2024 19:04
@sjrd
Copy link
MemberAuthor

sjrd commentedOct 2, 2024

@gzm0 I added a commit that introducesNewLambda as a new IR node that mimics aLambdaMetaFactory. It synthesizes the required anonymous classes at link-time, based on aDescriptor and a typed closure. That notably removes the need for the hard-codedTypedFunctionN classes. It is also generalizable to SAM types such asj.u.Comparator, although I did not implement that yet.

As the commit message says, the implementation on the linker side is butchered at the moment. The idea is to support discussion of the IR changes at the moment.

WDYT? If we add generalization to SAM types, this would also improve JS: all the lambdas forj.u.Comparator (for example) in the codebase would be able to share a unique anonymous class, like we currently do "by hand" forAnonFunctionNs.

Copy link
Contributor

@gzm0gzm0 left a comment

Choose a reason for hiding this comment

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

I think theNewLambda tree looks very promising, both in terms of simplifying the compiler and in giving more semantic information to the linker.

What I'm wondering is whether it is worth keeping theTypedClosure abstraction in the core IR: if we could only haveNewLambda the complexity increase in the IR would be minimal. (if it helps the optimizer, we could still consider introducing typed closures as transients).

@sjrd
Copy link
MemberAuthor

sjrd commentedOct 7, 2024

I think theNewLambda tree looks very promising, both in terms of simplifying the compiler and in giving more semantic information to the linker.

What I'm wondering is whether it is worth keeping theTypedClosure abstraction in the core IR: if we could only haveNewLambda the complexity increase in the IR would be minimal. (if it helps the optimizer, we could still consider introducing typed closures as transients).

I don't think we can removeTypedClosures. The deduplication ofNewLambdas needs them. If we don't have typed closures, then theNewLambda.Descriptor must also contain atarget method name, in addition to a list of capture param types. The problem is that those will be different at virtually every call site. That meansDescriptors are never the same, and every lambda will get its own class, defeating the entire purpose ofNewLambda.

@sjrdsjrdforce-pushed thetyped-closures branch 2 times, most recently from2e2513f tob23b5e5CompareOctober 8, 2024 09:04
@gzm0
Copy link
Contributor

gzm0 commentedOct 8, 2024

I don't think we can remove TypedClosures. The deduplication of NewLambdas needs them. If we don't have typed closures, then the NewLambda.Descriptor must also contain a target method name, in addition to a list of capture param types. The problem is that those will be different at virtually every call site. That means Descriptors are never the same, and every lambda will get its own class, defeating the entire purpose of NewLambda.

Ah, sorry, I wasn't clear. I understand that we need aclosure and adescriptor param toNewLambda and that we want the closure to be inline (sonot a reference). However, do we need to support first orderTypedClosures or can we only allow (and require) them as inputs toNewLambda? This would remove theTypedClosure type and theTypedClosureApply node.

It might mean that we have to fall back to a full class when we need bridges for SAMs, but maybe that's OK?

@sjrd
Copy link
MemberAuthor

sjrd commentedOct 9, 2024

Ah, I see. So something like

caseclassNewLambda(descriptor:Descriptor,captureParams:List[ParamDef],params:List[ParamDef],body:Tree,captureValues:List[Tree])

(note thatparams is required outside of theDescriptor to relate thenames of the params found inbody)

That can work from an IR-only point of view, indeed. The main issue is that it's not clear how theBaseLinker should then desugar theNewLambdas.

Currently it generates a field of aClosureType, a constructor taking aClosureType{,Ref} as argument, and anApplyTypedClosure. All of that is currently valid input to the IR checker, which can accurately validate that the closure types match. If we don't have closure types in the type system, it's unclear what to do. What type do we give to the field? How do we encode the type of the constructor parameter? Do we then needTransientType? If yes, how does the IR checker validate that transient types follow typing rules?

My hunch is that answering these questions will yield a design that is even more complicated than havingClosureTypes in the legitimate IR.

@gzm0
Copy link
Contributor

The main issue is that it's not clear how the BaseLinker should then desugar the NewLambdas.

Ah, I haven't looked that far. Are we sure we want to desugar the lambdas? It feels to me:

  • The optimizer would benefit from seeing lambdas (inlining hints, etc.)
  • The module splitter would benefit from seeing lambdas (there is a trade-off between duplicating a lambda sugar and introducing more module dependencies).
  • Expanding the lambdas in the backend only shouldn't be too hard?

@sjrd
Copy link
MemberAuthor

I'm pretty sure we want to desugar the lambdas. If we don't, then every analysis and optimization that relates to the class hierarchy analysis gets more complicated. Figuring out the analyzer's job, in particular in the presence of reflective proxy and/or default methods, gets really tricky. In the optimizer, a method call now needs to handle actual target methods as well as hidden methods of lambdas. Currently there are very few changes in the optimizer, and they're all somewhat duplicating forTypedClosures what we do forClosures. IfLambdas survive until then instead, that will complicate core mechanisms like function calls and inlining.

@gzm0
Copy link
Contributor

I see, yeah that makes sense. So maybe what could be an in-between option is to disallow (free-standing) TypedClosures trees and types in serialized IR? This is somewhat similar to records (whether to have a TypedClosure inside a NewLambda or flatten it is TBD).

This way, we can use TypedClosures in the linker pipeline but do not unnecessarily increase the IR interface.

@sjrd
Copy link
MemberAuthor

That seems doable. It will require a bit more work in the javalib IR cleaner (it currently lowers Scala functions to the underlying typed closures, but it could be custom functional interfaces instead), but nothing too dramatic.

@sjrd
Copy link
MemberAuthor

sjrd commentedFeb 6, 2025

@gzm0 This is now stand-alone and reviewable again, after rebasing on the merged#5101.

Copy link
Contributor

@gzm0gzm0 left a comment

Choose a reason for hiding this comment

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

Partial review. Sharing because I think it contains stuff worth iterating on already.

I have fully reviewed up until the "TODO: Continue here" comment in Analyzer.scala.

Copy link
MemberAuthor

@sjrdsjrd left a comment

Choose a reason for hiding this comment

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

Addressed comments inir/ andcompiler/ so far.

@gzm0
Copy link
Contributor

gzm0 commentedMar 2, 2025
edited
Loading

I just realized that I do not understand at all, why we still need custom box processing in GenJSCode with this change.

My understanding of the situation before this PR is that we use untyped closures to represent lambdas. As a result, we need to adjust boxes because the closure boundary requires boxed types (anys). However, with this change, we lift this limitation, so we should (?) be able to take the types scalac generates directly. As such, I would expect the necessary boxes to be already present.

Just TBC: I do understand that boxing/unboxing can/will be required between the specific interface a method implements and its implementation, and potentially even between theFunction node and the delambdafy target. What I do not understand is why the Scala.js backend needs to handle this at all (or does the JVM backend do similar things and it is "just like that"?).

I'll try to keep digging some more about this, but any help is appreciated.

@sjrd
Copy link
MemberAuthor

sjrd commentedMar 2, 2025

Yes, the JVM backend has to do a lot with that as well. Thebackend itself doesn't do much. The work is split between another phase (delambdafy, which is after our backend), and the JVM'sLambdaMetaFactory, which can handle boxes/unboxes that follow Java rules. We don't have the phase, and ourNewLambda doesn't deal with boxes, so the work to handle that is concentrated in the backend.

Copy link
Contributor

@gzm0gzm0 left a comment

Choose a reason for hiding this comment

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

Some replies + review of WASM (only nits there).

Copy link
MemberAuthor

@sjrdsjrd left a comment

Choose a reason for hiding this comment

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

I believed I have addressed all the comments so far.

The main question that is probably still under debate is the design ofSyntheticClassKind.

@sjrdsjrd requested a review fromgzm0March 3, 2025 16:39
Copy link
Contributor

@gzm0gzm0 left a comment

Choose a reason for hiding this comment

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

Some rebuttals and minor comments I found.

It's a bit tricky to review ATM because of the rebase on the name refactoring (using commits for both review history and intended target history at the same time is not working too well with Github's comment tracking).

But I think we can nevertheless make progress on the overall design here.

@sjrd
Copy link
MemberAuthor

sjrd commentedMar 9, 2025

It's a bit tricky to review ATM because of the rebase on the name refactoring (using commits for both review history and intended target history at the same time is not working too well with Github's comment tracking).

Sorry. I expected#5136 to be mostly a no-brainer that would be quickly reviewed and merged 😅

gzm0 reacted with laugh emoji

Copy link
Contributor

@gzm0gzm0 left a comment

Choose a reason for hiding this comment

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

Review of LambdaSynthesizer: Either there is a big problem with transient type refs, or there is a big chunk I do not understand :) either way, probably worth iterating (and I'm taking a break anyways :P).

for (intf <- descriptor.interfaces)
digestBuilder.updateUTF8String(intf.encoded)

// FIXME This is not efficient
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a reminder. OK to not address in this PR IMO.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

val suffixBuilder = new java.lang.StringBuilder(".$$Lambda$")
for (b <- digest) {
val i = b & 0xff
suffixBuilder.append(Character.forDigit(i >> 4, 16)).append(Character.forDigit(i & 0x0f, 16))
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI: I have considered whether we should factor out / share this withInternalModuleIDGenerator and I agree with the (implicit or explicit) decision not to.

def makeClassInfo(descriptor: NewLambda.Descriptor, className: ClassName): ClassInfo = {
val methodInfos = Array.fill(MemberNamespace.Count)(Map.empty[MethodName, MethodInfo])

val fFieldName = FieldName(className, SimpleFieldName("f"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: factor out the namef to deduplicate withmakeClassDef?

Copy link
Contributor

@gzm0gzm0 left a comment

Choose a reason for hiding this comment

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

Just some typos / optional things. LGTM to merge after squashing.

sjrd reacted with hooray emoji
* but we might as well cache them together.
*/
private val syntheticLambdaNamesCache =
mutable.Map.empty[NewLambda.Descriptor, (ClassName, MethodName)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: I agree with the design decision that the memory leak here is acceptable.

*
* Although `NewLambda` nodes themselves are desugared in the `Desugarer`,
* the corresponding synthetic *classes* already have an existence after the
* `BasedLinker`. They must, since they must participate in the CHA
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* `BasedLinker`.They must, since they must participate in theCHA
* `BaseLinker`.They must, since they must participate in theCRA

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

CHA = Class Hierarchy Analysis.
What is CRA?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems we have no tests at all for the new nodes.

Maybe that's OK in the first iteration: We've been working on this PR for quite a while. IDK. I'll leave it up to you.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment here than on ClassDefChecker test.

Seems we have no new tests at all, but maybe that's OK for now, so we can get this PR out.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

The `NewLambda` node creates an instance of an anonymous class froma `Descriptor` and a closure `fun`. The `Descriptor` specifies theshape of the anonymous class: a super class, a list of interfaces toimplement, and the name of a single non-constructor method to provide.The body of the method calls the `fun` closure.At link time, the Analyzer and BaseLinker synthesize a unique suchanonymous class per `Descriptor`. In practice, all the lambdas fora given target type share a common `Descriptor`. This is notably thecase for all the Scala functions of arity N.`NewLambda` replaces the need for special `AnonFunctionN` classes inthe library. Instead, classes of the right shape are synthesized atlink-time.The scheme can also be used for most LambdaMetaFactory-stylelambdas, although our `NewLambda` does not support bridgegeneration. In the common case where no bridges are necessary, wenow also generate a `NewLambda`. This generalizes the code sizeoptimization of having only one class per `Descriptor` to non-Scalafunctions.In order to truly support LMF-style lambdas, the closure `fun` musttake parameters that match the (erased) type in their superinterface.Previously, for Scala `FunctionN`, we knew by construction that theparameters and result types were always `any`, and so JS `Closure`swere good enough. Now, we need closures that can accept differenttypes. This is where typed closures come into play (see below).---When bridges are required, we still generate a custom class fromthe compiler backend. In that case, we statically inline the closurebody in the produced SAM implementation.We have to do this not to expose typed closures across method calls.Moreover, we need the better static types for the parameters to beable to inline the closures without too much hassle. So this change*has* to be done in lockstep with the rest of this commit.---A typed closure is a `Closure` that does not have any semantics forJS interop. This is stronger than `Char`, which is "merely" opaqueto JS. A `Char` can still be passed to JS and has a meaningful`toString()`. A typed closure *cannot* be passed to JS in any way.That is enforced by making their type *not* a subtype of `any`(like record types).Since a typed closure has no JS interop semantics, it is free tostrongly, statically type its parameters and result type.Additionally, we can freely choose its representation in the bestpossible way for the given target. On JS, that remains an arrowfunction. On Wasm, however, we represent it as a pair of`(capture data pointer, function pointer)`. This allows to compilethem in an efficient way that does not require going through a JSbridge closure. The latter has been shown to have a devastatingimpact on performance when a Scala function is used in a tightloop.The type of a typed closure is a `ClosureType`. It records itsparameter types and its result type. Closure types are non-variant:they are only subtypes of themselves. As mentioned, they are notsubtypes of `any`. They are however subtypes of `void` andsupertypes of `nothing`. Unfortunately, they must also be nullableto have a default value, so they have nullable and non-nullablealternatives.To call a typed closure, we introduce a dedicated application node`ApplyTypedClosure`. IR checking ensures that actual argumentsmatch the expected parameter types. The result type is directlyused as the type of the application.There are no changes to the source language. In particular, thereis no way to express typed closures or their types at the userlevel. They are only used for `NewLambda` nodes.In fact, typed closures and `ApplyTypedClosure`s are notfirst-class at the IR level. Before desugaring, typed closures areonly allowed as direct children of `NewLambda` nodes. Desugaringtransforms `NewLambda` nodes into `New`s of the synthesizedanonymous classes. At that point, the two typed closure nodesbecome first-class expression trees.---For Scala functions, these changes have no real impact on the JSoutput (only marginal naming differences). On Wasm, however, theymake Scala functions much, much faster. Before, a Scala function ina tight loop would cause a Wasm implementation to be, in the worstmeasured case, 20x slower than on JS. After these changes, similarbenchmarks become significantly faster on Wasm than on JS.
@sjrd
Copy link
MemberAuthor

Thanks a lot@gzm0 for reviewing this deep PR over the months!

tanishiking reacted with rocket emoji

@sjrdsjrd merged commitfec4ae7 intoscala-js:mainMar 16, 2025
3 checks passed
@sjrdsjrd deleted the typed-closures branchMarch 16, 2025 16:05
tanishiking added a commit to scala-wasm/scala-wasm that referenced this pull requestMar 17, 2025
Now JSArrayConstr forArray[T](...)Seq[T](...)List[T](...)is the problem
tanishiking added a commit to scala-wasm/scala-wasm that referenced this pull requestApr 7, 2025
Now JSArrayConstr forArray[T](...)Seq[T](...)List[T](...)is the problem
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@gzm0gzm0gzm0 approved these changes

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

2 participants
@sjrd@gzm0

[8]ページ先頭

©2009-2025 Movatter.jp