Skip to content

PoC: Support for function*/yield. #5082

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5374,6 +5374,28 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
*/
js.JSAwait(arg)

case JS_GENERATOR =>
// js.Generator.apply(arg)
assert(args.size == 1,
s"Expected exactly 1 argument for JS primitive $code but got " +
s"${args.size} at $pos")
val Block(stats, fun: Function) = args.head
val genStats = stats.map(genStat(_))
val asyncExpr = genAnonFunction(fun) match {
case js.NewLambda(_, closure: js.Closure)
if closure.params.size == 1 && closure.resultType == jstpe.AnyType =>
val newFlags = closure.flags.withTyped(false).withArrow(false).withGenerator(true)
js.JSFunctionApply(closure.copy(flags = newFlags), js.Undefined() :: Nil)
case other =>
abort(s"Unexpected tree generated for the Function1 argument to js.Generator.apply at $pos: $other")
}
js.Block(genStats, asyncExpr)

case JS_YIELD | JS_YIELD_STAR =>
// js.yield(arg, evidence)
val (arg, yieldEvidence) = genArgs2
js.JSYield(arg, star = code == JS_YIELD_STAR)

case DYNAMIC_IMPORT =>
assert(args.size == 1,
s"Expected exactly 1 argument for JS primitive $code but got " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ trait JSDefinitions {
lazy val JSConstructorTagModule = getRequiredModule("scala.scalajs.js.ConstructorTag")
lazy val JSConstructorTag_materialize = getMemberMethod(JSConstructorTagModule, newTermName("materialize"))

lazy val JSGeneratorModule = getRequiredModule("scala.scalajs.js.Generator")
lazy val JSGeneratorModuleClass = JSGeneratorModule.moduleClass
lazy val JSGenerator_apply = getMemberMethod(JSGeneratorModuleClass, nme.apply)
lazy val JSGenerator_yield = getMemberMethod(JSGeneratorModuleClass, newTermName("yield"))
lazy val JSGenerator_yield_* = getMemberMethod(JSGeneratorModuleClass, newTermName("yield_$times"))

lazy val JSNewModule = getRequiredModule("scala.scalajs.js.new")
lazy val JSNewModuleClass = JSNewModule.moduleClass
lazy val JSNew_target = getMemberMethod(JSNewModuleClass, newTermName("target"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ abstract class JSPrimitives {
final val JS_ASYNC = JS_IMPORT_META + 1 // js.async
final val JS_AWAIT = JS_ASYNC + 1 // js.await

final val CONSTRUCTOROF = JS_AWAIT + 1 // runtime.constructorOf(clazz)
final val JS_GENERATOR = JS_AWAIT + 1 // js.Generator.apply
final val JS_YIELD = JS_GENERATOR + 1 // js.Generator.yield
final val JS_YIELD_STAR = JS_YIELD + 1 // js.Generator.yield_*

final val CONSTRUCTOROF = JS_YIELD_STAR + 1 // runtime.constructorOf(clazz)
final val CREATE_INNER_JS_CLASS = CONSTRUCTOROF + 1 // runtime.createInnerJSClass
final val CREATE_LOCAL_JS_CLASS = CREATE_INNER_JS_CLASS + 1 // runtime.createLocalJSClass
final val WITH_CONTEXTUAL_JS_CLASS_VALUE = CREATE_LOCAL_JS_CLASS + 1 // runtime.withContextualJSClassValue
Expand Down Expand Up @@ -109,6 +113,10 @@ abstract class JSPrimitives {
addPrimitive(JSImport_apply, JS_IMPORT)
addPrimitive(JSImport_meta, JS_IMPORT_META)

addPrimitive(JSGenerator_apply, JS_GENERATOR)
addPrimitive(JSGenerator_yield, JS_YIELD)
addPrimitive(JSGenerator_yield_*, JS_YIELD_STAR)

addPrimitive(Runtime_constructorOf, CONSTRUCTOROF)
addPrimitive(Runtime_createInnerJSClass, CREATE_INNER_JS_CLASS)
addPrimitive(Runtime_createLocalJSClass, CREATE_LOCAL_JS_CLASS)
Expand Down
98 changes: 19 additions & 79 deletions examples/helloworld/src/main/scala/helloworld/HelloWorld.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,87 +9,27 @@ import scala.scalajs.js
import js.annotation._

object HelloWorld {
def main(args: Array[String]): Unit = {
import js.DynamicImplicits.truthValue

if (js.typeOf(js.Dynamic.global.document) != "undefined" &&
js.Dynamic.global.document &&
js.Dynamic.global.document.getElementById("playground")) {
sayHelloFromDOM()
sayHelloFromTypedDOM()
sayHelloFromJQuery()
sayHelloFromTypedJQuery()
} else {
println("Hello world!")
def myGenerator(n: Int): js.Generator[Int, String, Int] = js.Generator[Int, String, Int] { implicit ev =>
println("one")
js.Generator.`yield`(42)
println("two")
var i = 0
var j = 0
while (i != n) {
j += js.Generator.`yield`(j)
i += 1
}
"result"
}

def sayHelloFromDOM(): Unit = {
val document = js.Dynamic.global.document
val playground = document.getElementById("playground")

val newP = document.createElement("p")
newP.innerHTML = "Hello world! <i>-- DOM</i>"
playground.appendChild(newP)
}

def sayHelloFromTypedDOM(): Unit = {
val document = window.document
val playground = document.getElementById("playground")

val newP = document.createElement("p")
newP.innerHTML = "Hello world! <i>-- typed DOM</i>"
playground.appendChild(newP)
}

def sayHelloFromJQuery(): Unit = {
// val $ is fine too, but not very recommended in Scala code
val jQuery = js.Dynamic.global.jQuery
val newP = jQuery("<p>").html("Hello world! <i>-- jQuery</i>")
newP.appendTo(jQuery("#playground"))
}

def sayHelloFromTypedJQuery(): Unit = {
val jQuery = helloworld.JQuery
val newP = jQuery("<p>").html("Hello world! <i>-- typed jQuery</i>")
newP.appendTo(jQuery("#playground"))
def main(args: Array[String]): Unit = {
println("hello")

/*
// Works on JS but not on WebAssembly
val g = myGenerator(5)
for (k <- 0 until 8)
js.Dynamic.global.console.log(g.next(k))
*/
}
}

@js.native
@JSGlobalScope
object window extends js.Object {
val document: DOMDocument = js.native

def alert(msg: String): Unit = js.native
}

@js.native
trait DOMDocument extends js.Object {
def getElementById(id: String): DOMElement = js.native
def createElement(tag: String): DOMElement = js.native
}

@js.native
trait DOMElement extends js.Object {
var innerHTML: String = js.native

def appendChild(child: DOMElement): Unit = js.native
}

@js.native
@JSGlobal("jQuery")
object JQuery extends js.Object {
def apply(selector: String): JQuery = js.native
}

@js.native
trait JQuery extends js.Object {
def text(value: String): JQuery = js.native
def text(): String = js.native

def html(value: String): JQuery = js.native
def html(): String = js.native

def appendTo(parent: JQuery): JQuery = js.native
}
5 changes: 5 additions & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ object Hashers {
mixTag(TagJSAwait)
mixTree(arg)

case JSYield(arg, star) =>
mixTag(TagJSYield)
mixTree(arg)
mixBoolean(star)

case Debugger() =>
mixTag(TagDebugger)

Expand Down
10 changes: 10 additions & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Printers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ object Printers {
print(arg)
print(")")

case JSYield(arg, star) =>
if (star)
print("yield*(")
else
print("yield(")
print(arg)
print(")")

case Debugger() =>
print("debugger")

Expand Down Expand Up @@ -910,6 +918,8 @@ object Printers {
print("arrow-lambda")
else
print("lambda")
if (flags.generator)
print("*")
print("<")
var first = true
for ((param, value) <- captureParams.zip(captureValues)) {
Expand Down
4 changes: 2 additions & 2 deletions ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import java.util.concurrent.ConcurrentHashMap
import scala.util.matching.Regex

object ScalaJSVersions extends VersionChecks(
current = "1.19.1-SNAPSHOT",
binaryEmitted = "1.19"
current = "1.20.0-SNAPSHOT",
binaryEmitted = "1.20-SNAPSHOT"
)

/** Helper class to allow for testing of logic. */
Expand Down
7 changes: 7 additions & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ object Serializers {
writeTagAndPos(TagJSAwait)
writeTree(arg)

case JSYield(arg, star) =>
writeTagAndPos(TagJSYield)
writeTree(arg)
writeBoolean(star)

case Debugger() =>
writeTagAndPos(TagDebugger)

Expand Down Expand Up @@ -1223,6 +1228,8 @@ object Serializers {

case TagJSAwait =>
JSAwait(readTree())
case TagJSYield =>
JSYield(readTree(), readBoolean())

case TagDebugger => Debugger()

Expand Down
1 change: 1 addition & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Tags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ private[ir] object Tags {
final val TagApplyTypedClosure = TagLinkTimeProperty + 1
final val TagNewLambda = TagApplyTypedClosure + 1
final val TagJSAwait = TagNewLambda + 1
final val TagJSYield = TagJSAwait + 1

// Tags for member defs

Expand Down
3 changes: 3 additions & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ object Transformers {
case JSAwait(arg) =>
JSAwait(transform(arg))

case JSYield(arg, star) =>
JSYield(transform(arg), star)

// Scala expressions

case New(className, ctor, args) =>
Expand Down
3 changes: 3 additions & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ object Traversers {
case JSAwait(arg) =>
traverse(arg)

case JSYield(arg, star) =>
traverse(arg)

// Scala expressions

case New(_, _, args) =>
Expand Down
26 changes: 26 additions & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,17 @@ object Trees {
val tpe = AnyType
}

/** `yield arg` or `yield* arg`.
*
* This is directly equivalent to a JavaScript `yield`/`yield*` expression.
* This node is only valid within a [[Closure]] node with the `generator`
* flag.
*/
sealed case class JSYield(arg: Tree, star: Boolean)(implicit val pos: Position)
extends Tree {
val tpe = AnyType
}

sealed case class Debugger()(implicit val pos: Position) extends Tree {
val tpe = VoidType
}
Expand Down Expand Up @@ -1235,6 +1246,12 @@ object Trees {
* If `flags.async` is `true`, it is an `async` closure. Async closures
* return a `Promise` of their body, and can contain [[JSAwait]] nodes.
* `flags.typed` and `flags.async` cannot both be `true`.
*
* If `flags.generator` is `true`, it is a generator (`*`) closure.
* Generator closures return a `{,Async}Generator` instance for their body,
* and can contain [[JSYield]] nodes.
*
* `flags.arrow` and `flags.generator` cannot both be `true`.
*/
sealed case class Closure(flags: ClosureFlags, captureParams: List[ParamDef],
params: List[ParamDef], restParam: Option[ParamDef], resultType: Type,
Expand Down Expand Up @@ -1596,6 +1613,8 @@ object Trees {

def async: Boolean = (bits & AsyncBit) != 0

def generator: Boolean = (bits & GeneratorBit) != 0

def withArrow(arrow: Boolean): ClosureFlags =
if (arrow) new ClosureFlags(bits | ArrowBit)
else new ClosureFlags(bits & ~ArrowBit)
Expand All @@ -1607,6 +1626,10 @@ object Trees {
def withAsync(async: Boolean): ClosureFlags =
if (async) new ClosureFlags(bits | AsyncBit)
else new ClosureFlags(bits & ~AsyncBit)

def withGenerator(generator: Boolean): ClosureFlags =
if (generator) new ClosureFlags(bits | GeneratorBit)
else new ClosureFlags(bits & GeneratorBit)
}

object ClosureFlags {
Expand All @@ -1622,6 +1645,9 @@ object Trees {
private final val AsyncShift = 2
private final val AsyncBit = 1 << AsyncShift

private final val GeneratorShift = 3
private final val GeneratorBit = 1 << GeneratorShift

/** `function` closure base flags. */
final val function: ClosureFlags =
new ClosureFlags(0)
Expand Down
55 changes: 55 additions & 0 deletions library/src/main/scala/scala/scalajs/js/Generator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Scala.js (https://www.scala-js.org/)
*
* Copyright EPFL.
*
* Licensed under Apache License 2.0
* (https://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.scalajs.js

import scala.scalajs.js
import scala.scalajs.js.annotation._

/** <span class="badge badge-ecma6" style="float: right;">ECMAScript 6</span>
* JavaScript Generator.
*/
trait Generator[+Elem, +Result, -NextParam] extends js.Iterator[Elem] with js.Iterable[Elem] {
def next(): js.Generator.Entry[Elem, Result]
def next(param: NextParam): js.Generator.Entry[Elem, Result]
def `return`[R >: Result](value: R): js.Generator.Entry[Elem, R]
def `throw`(exception: scala.Any): js.Generator.Entry[Elem, Result]

@JSName(js.Symbol.iterator)
def jsIterator(): this.type
}

object Generator {
def apply[Elem, Result, NextParam](
body: YieldEvidence[Elem, NextParam] => Result): js.Generator[Elem, Result, NextParam] = {
throw new java.lang.Error("stub")
}

def `yield`[Elem, NextParam](value: Elem)(
implicit evidence: YieldEvidence[Elem, NextParam]): NextParam = {
throw new java.lang.Error("stub")
}

def yield_*[Elem, NextParam](values: js.Iterable[Elem])(
implicit evidence: YieldEvidence[Elem, NextParam]): NextParam = {
throw new java.lang.Error("stub")
}

/** Return value of `js.Generator.next`. */
trait Entry[+Elem, +Result] extends js.Iterator.Entry[Elem] {
/** The result value. Reading this value is only valid if done is true. */
@JSName("value")
def resultValue: Result
}

sealed trait YieldEvidence[Elem, NextParam] extends js.Any
}
Loading