diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index e3a39bd5aa..718146327c 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -4,5 +4,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - run: ./ci/check-cla.sh "${{ github.event.pull_request.user.login }}" + - name: Verify CLA + uses: scala/cla-checker@v1 + with: + author: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml new file mode 100644 index 0000000000..9b8170126d --- /dev/null +++ b/.github/workflows/windows-ci.yml @@ -0,0 +1,51 @@ +name: Windows CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + SBT_OPTS: '-Xmx6g -Xms1g -Xss4m' + +jobs: + build: + strategy: + matrix: + java: [ '8' ] + + # Use the latest supported version. We will be less affected by ambient changes + # due to the lack of pinning than by breakages because of changing version support. + runs-on: windows-latest + + steps: + - name: Set up git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - uses: actions/checkout@v3 + - name: Set up JDK ${{ matrix.java }} + uses: coursier/setup-action@v1 + with: + jvm: temurin:1.${{ matrix.java }} + apps: sbt + - uses: actions/setup-node@v4 + with: + node-version: '24.x' + cache: 'npm' + - name: npm install + run: npm install + + # Very far from testing everything, but at least it is a good sanity check + - name: Test suite + run: sbt testSuite2_12/test + - name: Linker test suite + run: sbt linker2_12/test + # partest is slow; only execute one test as a smoke test + - name: partest smoke test + run: sbt "partestSuite2_12/testOnly -- --fastOpt run/option-fold.scala" + # Module splitting has some logic for case-insensitive filesystems, which we must test on Windows + - name: Test suite with module splitting + run: sbt 'set testSuite.v2_12/scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule).withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("org.scalajs.testsuite"))))' testSuite2_12/test + shell: bash # for the special characters in the command diff --git a/CODINGSTYLE.md b/CODINGSTYLE.md index 47cb6a448a..eb79ec4986 100644 --- a/CODINGSTYLE.md +++ b/CODINGSTYLE.md @@ -227,7 +227,7 @@ value :: list ``` When calling a method declared with an empty pair of parentheses, always use `()`. -Not doing so causes (fatal) warnings when calling Scala-declared methods in Scala 2.13.3+. +Not doing so causes (fatal) warnings when calling Scala-declared methods in Scala 2.13.x. For consistency, we also apply this rule to all Java-defined methods, including `toString()`. ### Method definition diff --git a/Jenkinsfile b/Jenkinsfile index 141b4c1a1d..c1a4c70069 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -64,7 +64,7 @@ rm -rf $TEST_LOCAL_IVY_HOME mkdir $TEST_LOCAL_IVY_HOME ln -s "$LOC_IVY_HOME/cache" "$TEST_LOCAL_IVY_HOME/cache" -export SBT_OPTS="-J-Xmx5G -J-XX:MaxPermSize=512M -Dsbt.boot.directory=$LOC_SBT_BOOT -Dsbt.ivy.home=$TEST_LOCAL_IVY_HOME -Divy.home=$TEST_LOCAL_IVY_HOME -Dsbt.global.base=$LOC_SBT_BASE" +export SBT_OPTS="-J-Xmx5G -Dsbt.boot.directory=$LOC_SBT_BOOT -Dsbt.ivy.home=$TEST_LOCAL_IVY_HOME -Divy.home=$TEST_LOCAL_IVY_HOME -Dsbt.global.base=$LOC_SBT_BASE" export COURSIER_CACHE="$LOC_CS_CACHE" export NODE_PATH="$HOME/node_modules/" @@ -232,17 +232,6 @@ def Tasks = [ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ $testSuite$v/test && - sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - $testSuite$v/test && - sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSStage in Global := FullOptStage' \ - $testSuite$v/test && - sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - $testSuite$v/test && sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ $testSuite$v/test && @@ -274,6 +263,11 @@ def Tasks = [ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ + $testSuite$v/test && sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("org.scalajs.testsuite"))))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ @@ -312,20 +306,6 @@ def Tasks = [ 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set Seq(jsEnv in $testSuite.v$v := new NodeJSEnvForcePolyfills(ESVersion.$esVersion), MyScalaJSPlugin.wantSourceMaps in $testSuite.v$v := ("$esVersion" != "ES5_1"))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ ++$scala $testSuite$v/test ''', @@ -355,17 +335,6 @@ def Tasks = [ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion).withAllowBigIntsForLongs(true)))' \ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion).withAllowBigIntsForLongs(true)).withOptimizer(false))' \ @@ -401,57 +370,76 @@ def Tasks = [ npm install && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ helloworld$v/run && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSStage in Global := FullOptStage' \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withPrettyPrint(true))' \ helloworld$v/run && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in reversi.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ reversi$v/fastLinkJS \ reversi$v/fullLinkJS && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ - jUnitTestOutputsJVM$v/test jUnitTestOutputsJS$v/test testBridge$v/test \ - 'set scalaJSStage in Global := FullOptStage' jUnitTestOutputsJS$v/test testBridge$v/test && + 'set scalaJSLinkerConfig in jUnitTestOutputsJS.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ + 'set scalaJSLinkerConfig in testBridge.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ + jUnitTestOutputsJS$v/test testBridge$v/test \ + 'set scalaJSStage in Global := FullOptStage' \ + jUnitTestOutputsJS$v/test testBridge$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSStage in Global := FullOptStage' \ $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ 'set scalaJSStage in Global := FullOptStage' \ $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSStage in Global := FullOptStage' \ $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ testingExample$v/testHtml && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSStage in Global := FullOptStage' \ - testingExample$v/testHtml + testingExample$v/testHtml && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ + irJS$v/fastLinkJS ''', /* For the bootstrap tests to be able to call @@ -536,16 +524,13 @@ def Tasks = [ ] def mainJavaVersion = "1.8" -def otherJavaVersions = ["11", "16"] +def otherJavaVersions = ["11", "17", "21"] def allJavaVersions = otherJavaVersions.clone() allJavaVersions << mainJavaVersion -def mainScalaVersion = "2.12.19" -def mainScalaVersions = ["2.12.19", "2.13.13"] +def mainScalaVersion = "2.12.20" +def mainScalaVersions = ["2.12.20", "2.13.16"] def otherScalaVersions = [ - "2.12.2", - "2.12.3", - "2.12.5", "2.12.6", "2.12.7", "2.12.8", @@ -559,9 +544,7 @@ def otherScalaVersions = [ "2.12.16", "2.12.17", "2.12.18", - "2.13.0", - "2.13.1", - "2.13.2", + "2.12.19", "2.13.3", "2.13.4", "2.13.5", @@ -571,10 +554,13 @@ def otherScalaVersions = [ "2.13.9", "2.13.10", "2.13.11", - "2.13.12" + "2.13.12", + "2.12.13", + "2.12.14", + "2.12.15" ] -def scala3Version = "3.2.1" +def scala3Version = "3.6.3" def allESVersions = [ "ES5_1", @@ -586,6 +572,8 @@ def allESVersions = [ "ES2020", "ES2021" // We do not use anything specifically from ES2021, but always test the latest to avoid #4675 ] +def defaultESVersion = "ES2015" +def latestESVersion = "ES2021" // The 'quick' matrix def quickMatrix = [] @@ -597,11 +585,12 @@ mainScalaVersions.each { scalaVersion -> quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"]) quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "true", testSuite: "testSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "testSuite"]) - quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"]) - quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuiteEx"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, esVersion: defaultESVersion, testMinify: "false", testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, esVersion: latestESVersion, testMinify: "false", testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, esVersion: defaultESVersion, testMinify: "false", testSuite: "testSuiteEx"]) quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "scalaTestSuite"]) - quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, esVersion: defaultESVersion, testMinify: "false", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "bootstrap", scala: scalaVersion, java: mainJavaVersion]) quickMatrix.add([task: "partest-fastopt", scala: scalaVersion, java: mainJavaVersion, partestopts: ""]) quickMatrix.add([task: "partest-fastopt", scala: scalaVersion, java: mainJavaVersion, partestopts: "--wasm"]) @@ -611,7 +600,10 @@ allESVersions.each { esVersion -> } allJavaVersions.each { javaVersion -> // the `scala` version is irrelevant here - quickMatrix.add([task: "sbt-plugin-and-scalastyle", scala: mainScalaVersion, java: javaVersion]) + // We exclude JDK 21 because our sbt scripted tests use old sbt versions (on purpose), which do not support JDK 21 + if (javaVersion != '21') { + quickMatrix.add([task: "sbt-plugin-and-scalastyle", scala: mainScalaVersion, java: javaVersion]) + } } quickMatrix.add([task: "scala3-compat", scala: scala3Version, java: mainJavaVersion]) @@ -623,7 +615,7 @@ otherScalaVersions.each { scalaVersion -> mainScalaVersions.each { scalaVersion -> otherJavaVersions.each { javaVersion -> quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: javaVersion, testMinify: "false", testSuite: "testSuite"]) - quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, esVersion: defaultESVersion, testMinify: "false", testSuite: "testSuite"]) } fullMatrix.add([task: "partest-noopt", scala: scalaVersion, java: mainJavaVersion, partestopts: ""]) fullMatrix.add([task: "partest-noopt", scala: scalaVersion, java: mainJavaVersion, partestopts: "--wasm"]) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4f0c93e14c..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: '{build}' -image: Visual Studio 2015 -environment: - global: - NODEJS_VERSION: "16" - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 -install: - - ps: Install-Product node $env:NODEJS_VERSION - - npm install - - cmd: choco install sbt --version 1.3.12 -ia "INSTALLDIR=""C:\sbt""" - - cmd: SET PATH=C:\sbt\bin;%JAVA_HOME%\bin;%PATH% - - cmd: SET "SBT_OPTS=-Xmx4g -Xms4m" -build: off -test_script: - # Very far from testing everything, but at least it is a good sanity check - # For slow things (partest and scripted), we execute only one test - - cmd: sbt ";clean;testSuite2_12/test;linker2_12/test;partestSuite2_12/testOnly -- --fastOpt run/option-fold.scala" - # Module splitting has some logic for case-insensitive filesystems, which we must test on Windows - - cmd: sbt ";setSmallESModulesForAppVeyorCI;testSuite2_12/test" -cache: - - C:\sbt - - C:\Users\appveyor\.ivy2\cache - - C:\Users\appveyor\.sbt diff --git a/ci/check-cla.sh b/ci/check-cla.sh deleted file mode 100755 index 05fc9ad6b0..0000000000 --- a/ci/check-cla.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -eux - -AUTHOR="$1" -echo "Pull request submitted by $AUTHOR"; -URL_AUTHOR=$(jq -rn --arg x "$AUTHOR" '$x|@uri') -signed=$(curl -s "https://www.lightbend.com/contribute/cla/scala/check/$URL_AUTHOR" | jq -r ".signed"); -if [ "$signed" = "true" ] ; then - echo "CLA check for $AUTHOR successful"; -else - echo "CLA check for $AUTHOR failed"; - echo "Please sign the Scala CLA to contribute to Scala.js."; - echo "Go to https://www.lightbend.com/contribute/cla/scala and then"; - echo "comment on the pull request to ask for a new check."; - exit 1; -fi; diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala b/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala index af59c2b4d0..2c8951a67f 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala @@ -71,11 +71,6 @@ trait CompatComponent { lazy val DottyEnumSingletonCompat = AttachmentsCompat.Inner.DottyEnumSingletonAlias - implicit final class SAMFunctionCompatOps(self: SAMFunction) { - // Introduced in 2.12.5 to synthesize bridges in LMF classes - def synthCls: Symbol = NoSymbol - } - /* global.genBCode.bTypes.initializeCoreBTypes() * Early 2.12.x versions require that this method be called from * GenJSCode.run(), but it disappeared later in the 2.12.x series. diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index ef5d20f120..2c39124753 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -26,8 +26,23 @@ import scala.annotation.tailrec import scala.reflect.internal.Flags import org.scalajs.ir -import org.scalajs.ir.{Trees => js, Types => jstpe, ClassKind, Hashers, OriginalName} -import org.scalajs.ir.Names.{LocalName, SimpleFieldName, FieldName, SimpleMethodName, MethodName, ClassName} +import org.scalajs.ir.{ + Trees => js, + Types => jstpe, + WellKnownNames => jswkn, + ClassKind, + Hashers, + OriginalName +} +import org.scalajs.ir.Names.{ + LocalName, + LabelName, + SimpleFieldName, + FieldName, + SimpleMethodName, + MethodName, + ClassName +} import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.Trees.OptimizerHints import org.scalajs.ir.Version.Unversioned @@ -151,6 +166,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val currentClassSym = new ScopedVar[Symbol] private val fieldsMutatedInCurrentClass = new ScopedVar[mutable.Set[Name]] private val generatedSAMWrapperCount = new ScopedVar[VarBox[Int]] + private val delambdafyTargetDefDefs = new ScopedVar[mutable.Map[Symbol, DefDef]] + private val methodsAllowingJSAwait = new ScopedVar[mutable.Set[Symbol]] def currentThisTypeNullable: jstpe.Type = encodeClassType(currentClassSym) @@ -158,7 +175,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) def currentThisType: jstpe.Type = { currentThisTypeNullable match { case tpe @ jstpe.ClassType(cls, _) => - jstpe.BoxedClassToPrimType.getOrElse(cls, tpe.toNonNullable) + jswkn.BoxedClassToPrimType.getOrElse(cls, tpe.toNonNullable) case tpe @ jstpe.AnyType => // We are in a JS class, in which even `this` is nullable tpe @@ -170,17 +187,19 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Per method body private val currentMethodSym = new ScopedVar[Symbol] - private val thisLocalVarIdent = new ScopedVar[Option[js.LocalIdent]] + private val thisLocalVarName = new ScopedVar[Option[LocalName]] private val enclosingLabelDefInfos = new ScopedVar[Map[Symbol, EnclosingLabelDefInfo]] private val isModuleInitialized = new ScopedVar[VarBox[Boolean]] private val undefinedDefaultParams = new ScopedVar[mutable.Set[Symbol]] private val mutableLocalVars = new ScopedVar[mutable.Set[Symbol]] private val mutatedLocalVars = new ScopedVar[mutable.Set[Symbol]] - private def withPerMethodBodyState[A](methodSym: Symbol)(body: => A): A = { + private def withPerMethodBodyState[A](methodSym: Symbol, + initThisLocalVarName: Option[LocalName] = None)(body: => A): A = { + withScopedVars( currentMethodSym := methodSym, - thisLocalVarIdent := None, + thisLocalVarName := initThisLocalVarName, enclosingLabelDefInfos := Map.empty, isModuleInitialized := new VarBox(false), undefinedDefaultParams := mutable.Set.empty, @@ -192,8 +211,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } // For anonymous methods - // These have a default, since we always read them. - private val tryingToGenMethodAsJSFunction = new ScopedVar[Boolean](false) + // It has a default, since we always read it. private val paramAccessorLocals = new ScopedVar(Map.empty[Symbol, js.ParamDef]) /* Contextual JS class value for some operations of nested JS classes that @@ -211,11 +229,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } - private class CancelGenMethodAsJSFunction(message: String) - extends scala.util.control.ControlThrowable { - override def getMessage(): String = message - } - // Rewriting of anonymous function classes --------------------------------- /** Start nested generation of a class. @@ -228,18 +241,146 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) currentClassSym := clsSym, fieldsMutatedInCurrentClass := mutable.Set.empty, generatedSAMWrapperCount := new VarBox(0), + delambdafyTargetDefDefs := mutable.Map.empty, + methodsAllowingJSAwait := mutable.Set.empty, currentMethodSym := null, - thisLocalVarIdent := null, + thisLocalVarName := null, enclosingLabelDefInfos := null, isModuleInitialized := null, undefinedDefaultParams := null, mutableLocalVars := null, mutatedLocalVars := null, - tryingToGenMethodAsJSFunction := false, paramAccessorLocals := Map.empty )(withNewLocalNameScope(body)) } + // Global state for tracking methods that reference `this` ----------------- + + /* scalac generates private instance methods for + * a) local defs and + * b) anonymous functions. + * + * In many cases, these methods do not need access to the enclosing `this` + * value. For every other local variable, `lambdalift` only adds them as + * arguments when they are needed; but `this` is always implicitly added + * because all such methods are instance methods. + * + * We run a separate analysis to figure out which of those methods actually + * need their `this` value. We compile those that do not as static methods, + * as if they were `isStaticMember`. + * + * The `delambdafy` phase of scalac performs a similar analysis, although + * as it runs after our backend (for other reasons), we do not benefit from + * it. Our analysis is much simpler because it deals with local defs and + * anonymous functions in a unified way. + * + * Performing this analysis is particularly important for lifted methods + * that appear within arguments to a super constructor call. The lexical + * scope of Scala guarantees that they cannot use `this`, but if they are + * compiled as instance methods, they force the constructor to leak the + * `this` value before the super constructor call, which is invalid. + * While most of the time this analysis is only an optimization, in those + * (rare) situations it is required for correctness. See for example + * the partest `run/t8733.scala`. + */ + + private var statifyCandidateMethodsThatReferenceThis: scala.collection.Set[Symbol] = null + + /** Is that given method a statify-candidate? + * + * If a method is a statify-candidate, we will analyze whether it actually + * needs its `this` value. If it does not, we will compile it as a static + * method in the IR. + * + * A method is a statify-candidate if it is a lifted method (a local def) + * or the target of an anonymous function. + * + * TODO Currently we also require that the method owner not be a JS type. + * We should relax that restriction in the future. + */ + private def isStatifyCandidate(sym: Symbol): Boolean = + (sym.isLiftedMethod || sym.isDelambdafyTarget) && !isJSType(sym.owner) + + /** Do we compile the given method as a static method in the IR? + * + * This is true if one of the following is true: + * + * - It is `isStaticMember`, or + * - It is a statify-candidate method and it does not reference `this`. + */ + private def compileAsStaticMethod(sym: Symbol): Boolean = { + sym.isStaticMember || { + isStatifyCandidate(sym) && + !statifyCandidateMethodsThatReferenceThis.contains(sym) + } + } + + /** Finds all statify-candidate methods that reference `this`, inspired by Delambdafy. */ + private final class ThisReferringMethodsTraverser extends Traverser { + // the set of statify-candidate methods that directly refer to `this` + private val roots = mutable.Set.empty[Symbol] + + // for each statify-candidate method `m`, the set of statify-candidate methods that call `m` + private val methodReferences = mutable.Map.empty[Symbol, mutable.Set[Symbol]] + + def methodReferencesThisIn(tree: Tree): collection.Set[Symbol] = { + traverse(tree) + propagateReferences() + } + + private def addRoot(symbol: Symbol): Unit = + roots += symbol + + private def addReference(from: Symbol, to: Symbol): Unit = + methodReferences.getOrElseUpdate(to, mutable.Set.empty) += from + + private def propagateReferences(): collection.Set[Symbol] = { + val result = mutable.Set.empty[Symbol] + + def rec(symbol: Symbol): Unit = { + // mark `symbol` as referring to `this` + if (result.add(symbol)) { + // `symbol` was not yet in the set; propagate further + methodReferences.getOrElse(symbol, Nil).foreach(rec(_)) + } + } + + roots.foreach(rec(_)) + + result + } + + private var currentMethod: Symbol = NoSymbol + + override def traverse(tree: Tree): Unit = tree match { + case _: DefDef => + if (isStatifyCandidate(tree.symbol)) { + currentMethod = tree.symbol + super.traverse(tree) + currentMethod = NoSymbol + } else { + // No need to traverse other methods; we always assume they refer to `this`. + } + case Function(_, Apply(target, _)) => + /* We don't drill into functions because at this phase they will always refer to `this`. + * They will be of the form {(args...) => this.anonfun(args...)} + * but we do need to make note of the lifted body method in case it refers to `this`. + */ + if (currentMethod.exists) + addReference(from = currentMethod, to = target.symbol) + case Apply(sel @ Select(This(_), _), args) if isStatifyCandidate(sel.symbol) => + if (currentMethod.exists) + addReference(from = currentMethod, to = sel.symbol) + super.traverseTrees(args) + case This(_) => + // Note: tree.symbol != enclClass is possible if it is a module loaded with genLoadModule + if (currentMethod.exists && tree.symbol == currentMethod.enclClass) + addRoot(currentMethod) + case _ => + super.traverse(tree) + } + } + // Global class generation state ------------------------------------------- private val lazilyGeneratedAnonClasses = mutable.Map.empty[Symbol, ClassDef] @@ -247,21 +388,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private val generatedStaticForwarderClasses = ListBuffer.empty[(Symbol, js.ClassDef)] private def consumeLazilyGeneratedAnonClass(sym: Symbol): ClassDef = { - /* If we are trying to generate an method as JSFunction, we cannot - * actually consume the symbol, since we might fail trying and retry. - * We will then see the same tree again and not find the symbol anymore. - * - * If we are sure this is the only generation, we remove the symbol to - * make sure we don't generate the same class twice. - */ - val optDef = { - if (tryingToGenMethodAsJSFunction) - lazilyGeneratedAnonClasses.get(sym) - else - lazilyGeneratedAnonClasses.remove(sym) - } - - optDef.getOrElse { + lazilyGeneratedAnonClasses.remove(sym).getOrElse { abort("Couldn't find tree for lazily generated anonymous class " + s"${sym.fullName} at ${sym.pos}") } @@ -298,6 +425,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) */ override def apply(cunit: CompilationUnit): Unit = { try { + statifyCandidateMethodsThatReferenceThis = + new ThisReferringMethodsTraverser().methodReferencesThisIn(cunit.body) + def collectClassDefs(tree: Tree): List[ClassDef] = { tree match { case EmptyTree => Nil @@ -307,25 +437,22 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } val allClassDefs = collectClassDefs(cunit.body) - /* There are three types of anonymous classes we want to generate - * only once we need them so we can inline them at construction site: + /* There are two types of anonymous classes we want to generate only + * once we need them, so we can inline them at construction site: * - * - anonymous class that are JS types, which includes: - * - lambdas for js.FunctionN and js.ThisFunctionN (SAMs). (We may - * not generate actual Scala classes for these). - * - anonymous (non-lambda) JS classes. These classes may not have - * their own prototype. Therefore, their constructor *must* be - * inlined. - * - lambdas for scala.FunctionN. This is only an optimization and may - * fail. In the case of failure, we fall back to generating a - * fully-fledged Scala class. + * - Lambdas for `js.Function` SAMs, including `js.FunctionN`, + * `js.ThisFunctionN` and custom JS function SAMs. We must generate + * JS functions for these, instead of actual classes. + * - Anonymous (non-lambda) JS classes. These classes may not have + * their own prototype. Therefore, their constructor *must* be + * inlined. * * Since for all these, we don't know how they inter-depend, we just * store them in a map at this point. */ val (lazyAnons, fullClassDefs) = allClassDefs.partition { cd => val sym = cd.symbol - isAnonymousJSClass(sym) || isJSFunctionDef(sym) || sym.isAnonymousFunction + isAnonymousJSClass(sym) || isJSFunctionDef(sym) } lazilyGeneratedAnonClasses ++= lazyAnons.map(cd => cd.symbol -> cd) @@ -343,7 +470,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) withScopedVars( currentClassSym := sym, fieldsMutatedInCurrentClass := mutable.Set.empty, - generatedSAMWrapperCount := new VarBox(0) + generatedSAMWrapperCount := new VarBox(0), + delambdafyTargetDefDefs := mutable.Map.empty, + methodsAllowingJSAwait := mutable.Set.empty ) { val tree = if (isJSType(sym)) { if (!sym.isTraitOrInterface && isNonNativeJSClass(sym) && @@ -418,20 +547,26 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) genIRFile(cunit, hashedClassDef) } catch { case e: ir.InvalidIRException => - e.tree match { - case ir.Trees.Transient(UndefinedParam) => + e.optTree match { + case Some(tree @ ir.Trees.Transient(UndefinedParam)) => reporter.error(pos, "Found a dangling UndefinedParam at " + - s"${e.tree.pos}. This is likely due to a bad " + + s"${tree.pos}. This is likely due to a bad " + "interaction between a macro or a compiler plugin " + "and the Scala.js compiler plugin. If you hit " + "this, please let us know.") - case _ => + case Some(tree) => reporter.error(pos, "The Scala.js compiler generated invalid IR for " + "this class. Please report this as a bug. IR: " + - e.tree) + tree) + + case None => + reporter.error(pos, + "The Scala.js compiler generated invalid IR for this class. " + + "Please report this as a bug. " + + e.getMessage()) } } } @@ -447,10 +582,39 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) lazilyGeneratedAnonClasses.clear() generatedStaticForwarderClasses.clear() generatedClasses.clear() + statifyCandidateMethodsThatReferenceThis = null pos2irPosCache.clear() } } + private def collectDefDefs(impl: Template): List[DefDef] = { + val b = List.newBuilder[DefDef] + + for (stat <- impl.body) { + stat match { + case stat: DefDef => + if (stat.symbol.isDelambdafyTarget) + delambdafyTargetDefDefs += stat.symbol -> stat + else + b += stat + + case EmptyTree | _:ValDef => + () + + case _ => + abort(s"Unexpected tree in template: $stat at ${stat.pos}") + } + } + + b.result() + } + + private def consumeDelambdafyTarget(sym: Symbol): DefDef = { + delambdafyTargetDefDefs.remove(sym).getOrElse { + abort(s"Cannot resolve delambdafy target $sym at ${sym.pos}") + } + } + // Generate a class -------------------------------------------------------- /** Gen the IR ClassDef for a class definition (maybe a module class). @@ -501,26 +665,13 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val methodsBuilder = List.newBuilder[js.MethodDef] val jsNativeMembersBuilder = List.newBuilder[js.JSNativeMemberDef] - def gen(tree: Tree): Unit = { - tree match { - case EmptyTree => () - case Template(_, _, body) => body foreach gen - - case ValDef(mods, name, tpt, rhs) => - () // fields are added via genClassFields() - - case dd: DefDef => - if (dd.symbol.hasAnnotation(JSNativeAnnotation)) - jsNativeMembersBuilder += genJSNativeMemberDef(dd) - else - methodsBuilder ++= genMethod(dd) - - case _ => abort("Illegal tree in gen of genClass(): " + tree) - } + for (dd <- collectDefDefs(impl)) { + if (dd.symbol.hasAnnotation(JSNativeAnnotation)) + jsNativeMembersBuilder += genJSNativeMemberDef(dd) + else + methodsBuilder ++= genMethod(dd) } - gen(impl) - val fields = if (!isHijacked) genClassFields(cd) else Nil val jsNativeMembers = jsNativeMembersBuilder.result() @@ -556,7 +707,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) reflectInit.toList ::: staticModuleInit.toList if (staticInitializerStats.nonEmpty) { List(genStaticConstructorWithStats( - ir.Names.StaticInitializerName, + jswkn.StaticInitializerName, js.Block(staticInitializerStats))) } else { Nil @@ -587,7 +738,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) originalName, ClassKind.Class, None, - Some(js.ClassIdent(ir.Names.ObjectClass)), + Some(js.ClassIdent(jswkn.ObjectClass)), Nil, None, None, @@ -658,44 +809,31 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val generatedMethods = new ListBuffer[js.MethodDef] val dispatchMethodNames = new ListBuffer[JSName] - def gen(tree: Tree): Unit = { - tree match { - case EmptyTree => () - case Template(_, _, body) => body foreach gen - - case ValDef(mods, name, tpt, rhs) => - () // fields are added via genClassFields() - - case dd: DefDef => - val sym = dd.symbol - val exposed = isExposed(sym) + for (dd <- collectDefDefs(cd.impl)) { + val sym = dd.symbol + val exposed = isExposed(sym) - if (sym.isClassConstructor) { - constructorTrees += dd - } else if (exposed && sym.isAccessor && !sym.isLazy) { - /* Exposed accessors must not be emitted, since the field they - * access is enough. - */ - } else if (sym.hasAnnotation(JSOptionalAnnotation)) { - // Optional methods must not be emitted - } else { - generatedMethods ++= genMethod(dd) - - // Collect the names of the dispatchers we have to create - if (exposed && !sym.isDeferred) { - /* We add symbols that we have to expose here. This way we also - * get inherited stuff that is implemented in this class. - */ - dispatchMethodNames += jsNameOf(sym) - } - } + if (sym.isClassConstructor) { + constructorTrees += dd + } else if (exposed && sym.isAccessor && !sym.isLazy) { + /* Exposed accessors must not be emitted, since the field they + * access is enough. + */ + } else if (sym.hasAnnotation(JSOptionalAnnotation)) { + // Optional methods must not be emitted + } else { + generatedMethods ++= genMethod(dd) - case _ => abort("Illegal tree in gen of genClass(): " + tree) + // Collect the names of the dispatchers we have to create + if (exposed && !sym.isDeferred) { + /* We add symbols that we have to expose here. This way we also + * get inherited stuff that is implemented in this class. + */ + dispatchMethodNames += jsNameOf(sym) + } } } - gen(cd.impl) - // Static members (exported from the companion object) val (staticFields, staticExports) = { /* Phase travel is necessary for non-top-level classes, because flatten @@ -715,7 +853,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (staticFields.nonEmpty) { generatedMethods += genStaticConstructorWithStats( - ir.Names.ClassInitializerName, genLoadModule(companionModuleClass)) + jswkn.ClassInitializerName, genLoadModule(companionModuleClass)) } (staticFields, staticExports) @@ -820,7 +958,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Make new class def with static members val newClassDef = { implicit val pos = origJsClass.pos - val parent = js.ClassIdent(ir.Names.ObjectClass) + val parent = js.ClassIdent(jswkn.ObjectClass) js.ClassDef(origJsClass.name, origJsClass.originalName, ClassKind.AbstractJSType, None, Some(parent), interfaces = Nil, jsSuperClass = None, jsNativeLoadSpec = None, fields = Nil, @@ -853,14 +991,14 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * FIXME This could clash with a local variable of the constructor or a JS * class capture. How do we avoid this? */ - val selfName = freshLocalIdent("this")(pos) + val selfIdent = freshLocalIdent("this")(pos) def selfRef(implicit pos: ir.Position) = - js.VarRef(selfName)(jstpe.AnyType) + js.VarRef(selfIdent.name)(jstpe.AnyType) def memberLambda(params: List[js.ParamDef], restParam: Option[js.ParamDef], body: js.Tree)(implicit pos: ir.Position) = { - js.Closure(arrow = false, captureParams = Nil, params, restParam, body, - captureValues = Nil) + js.Closure(js.ClosureFlags.function, captureParams = Nil, params, + restParam, jstpe.AnyType, body, captureValues = Nil) } val fieldDefinitions = jsFieldDefs.toList.map { fdef => @@ -955,30 +1093,26 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.JSNew(jsSuperClassRef, args) } - val selfVarDef = js.VarDef(selfName, thisOriginalName, jstpe.AnyType, mutable = false, newTree) + val selfVarDef = js.VarDef(selfIdent.copy(), // copy for the correct `pos` + thisOriginalName, jstpe.AnyType, mutable = false, newTree) selfVarDef :: memberDefinitions } - // After the super call, substitute `selfRef` for `This()` - val afterSuper = new ir.Transformers.Transformer { - override def transform(tree: js.Tree, isStat: Boolean): js.Tree = tree match { + // After the super call, substitute `selfRef` for `this` + val afterSuper = new ir.Transformers.LocalScopeTransformer { + override def transform(tree: js.Tree): js.Tree = tree match { case js.This() => selfRef(tree.pos) - - // Don't traverse closure boundaries - case closure: js.Closure => - val newCaptureValues = closure.captureValues.map(transformExpr) - closure.copy(captureValues = newCaptureValues)(closure.pos) - case tree => - super.transform(tree, isStat) + super.transform(tree) } - }.transformStats(ctorBody.afterSuper) + }.transformTrees(ctorBody.afterSuper) beforeSuper ::: superCall ::: afterSuper } - val closure = js.Closure(arrow = true, jsClassCaptures, Nil, None, + // Wrap everything in a lambda, for namespacing + val closure = js.Closure(js.ClosureFlags.arrow, jsClassCaptures, Nil, None, jstpe.AnyType, js.Block(inlinedCtorStats, selfRef), jsSuperClassValue :: args) js.JSFunctionApply(closure, Nil) } @@ -1019,20 +1153,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val classIdent = encodeClassNameIdent(sym) - // fill in class info builder - def gen(tree: Tree): List[js.MethodDef] = { - tree match { - case EmptyTree => Nil - case Template(_, _, body) => body.flatMap(gen) - - case dd: DefDef => - genMethod(dd).toList - - case _ => - abort("Illegal tree in gen of genInterface(): " + tree) - } - } - val generatedMethods = gen(cd.impl) + val generatedMethods = collectDefDefs(cd.impl).flatMap(genMethod(_)) val interfaces = genClassInterfaces(sym, forJSClass = false) val allMemberDefs = @@ -1136,16 +1257,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) assert(moduleClass.isModuleClass, moduleClass) - val hasAnyExistingPublicStaticMethod = - existingMethods.exists(_.flags.namespace == js.MemberNamespace.PublicStatic) - if (hasAnyExistingPublicStaticMethod) { - reporter.error(pos, - "Unexpected situation: found existing public static methods in " + - s"the class ${moduleClass.fullName} while trying to generate " + - "static forwarders for its companion object. " + - "Please report this as a bug in Scala.js.") - } - def listMembersBasedOnFlags = { // Copy-pasted from BCodeHelpers. val ExcludedForwarderFlags: Long = { @@ -1285,7 +1396,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.MethodIdent(name), NoOriginalName, Nil, - jstpe.NoType, + jstpe.VoidType, Some(stats))( OptimizerHints.empty, Unversioned) } @@ -1307,7 +1418,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val fqcnArg = js.StringLiteral(sym.fullName + "$") val runtimeClassArg = js.ClassOf(toTypeRef(sym.info)) val loadModuleFunArg = - js.Closure(arrow = true, Nil, Nil, None, genLoadModule(sym), Nil) + js.Closure(js.ClosureFlags.arrow, Nil, Nil, None, jstpe.AnyType, genLoadModule(sym), Nil) val stat = genApplyMethod( genLoadModule(ReflectModule), @@ -1357,9 +1468,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val paramTypesArray = js.JSArrayConstr(parameterTypes) - val newInstanceFun = js.Closure(arrow = true, Nil, formalParams, None, { - genNew(sym, ctor, actualParams) - }, Nil) + val newInstanceFun = js.Closure(js.ClosureFlags.arrow, Nil, + formalParams, None, jstpe.AnyType, genNew(sym, ctor, actualParams), Nil) js.JSArrayConstr(List(paramTypesArray, newInstanceFun)) } @@ -1481,7 +1591,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) var preSuperStats = List.newBuilder[js.Tree] var jsSuperCall: Option[js.JSSuperConstructorCall] = None - val postSuperStats = List.newBuilder[js.Tree] + val postSuperStats = mutable.ListBuffer.empty[js.Tree] /* Move param accessor initializers and early initializers after the * super constructor call since JS cannot access `this` before the super @@ -1585,6 +1695,13 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) assert(jsSuperCall.isDefined, "Did not find Super call in primary JS " + s"construtor at ${dd.pos}") + /* Insert a StoreModule if required. + * Do this now so we have the pos of the super ctor call. + * +=: prepends to the ListBuffer in O(1) -- yes, it's a cryptic name. + */ + if (isStaticModule(currentClassSym)) + js.StoreModule()(jsSuperCall.get.pos) +=: postSuperStats + new PrimaryJSCtor(sym, genParamsAndInfo(sym, vparamss), js.JSConstructorBody(preSuperStats.result(), jsSuperCall.get, postSuperStats.result())(dd.pos)) } @@ -1713,7 +1830,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } - js.If(cond, body, js.Skip())(jstpe.NoType) + js.If(cond, body, js.Skip())(jstpe.VoidType) } /* preStats / postStats use pre/post order traversal respectively to @@ -1909,11 +2026,13 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * * Other (normal) methods are emitted with `genMethodDef()`. */ - def genMethodWithCurrentLocalNameScope(dd: DefDef): js.MethodDef = { + def genMethodWithCurrentLocalNameScope(dd: DefDef, + initThisLocalVarName: Option[LocalName] = None): js.MethodDef = { + implicit val pos = dd.pos val sym = dd.symbol - withPerMethodBodyState(sym) { + withPerMethodBodyState(sym, initThisLocalVarName) { val methodName = encodeMethodSym(sym) val originalName = originalNameOfMethod(sym) @@ -1932,7 +2051,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } else { val shouldMarkInline = { sym.hasAnnotation(InlineAnnotationClass) || - sym.name.startsWith(nme.ANON_FUN_NAME) || + sym.name.containsName(nme.ANON_FUN_NAME) || adHocInlineMethods.contains(sym.fullName) } @@ -1951,12 +2070,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val namespace = js.MemberNamespace.Constructor js.MethodDef( js.MemberFlags.empty.withNamespace(namespace), methodName, - originalName, jsParams, jstpe.NoType, Some(genStat(dd.rhs)))( + originalName, jsParams, jstpe.VoidType, Some(genStat(dd.rhs)))( optimizerHints, Unversioned) } else { val resultIRType = toIRType(sym.tpe.resultType) val namespace = { - if (sym.isStaticMember) { + if (compileAsStaticMethod(sym)) { if (sym.isPrivate) js.MemberNamespace.PrivateStatic else js.MemberNamespace.PublicStatic } else { @@ -1980,8 +2099,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) methodDef } else { val patches = ( - unmutatedMutableLocalVars.map(encodeLocalSym(_).name -> false) ::: - mutatedImmutableLocalVals.map(encodeLocalSym(_).name -> true) + unmutatedMutableLocalVars.map(encodeLocalSymName(_) -> false) ::: + mutatedImmutableLocalVals.map(encodeLocalSymName(_) -> true) ).toMap patchMutableFlagOfLocals(methodDef, patches) } @@ -2037,21 +2156,16 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } yield { js.ParamDef(name, originalName, ptpe, newMutable(name.name, mutable))(p.pos) } - val transformer = new ir.Transformers.Transformer { - override def transform(tree: js.Tree, isStat: Boolean): js.Tree = tree match { + val transformer = new ir.Transformers.LocalScopeTransformer { + override def transform(tree: js.Tree): js.Tree = tree match { case js.VarDef(name, originalName, vtpe, mutable, rhs) => - assert(isStat, s"found a VarDef in expression position at ${tree.pos}") super.transform(js.VarDef(name, originalName, vtpe, - newMutable(name.name, mutable), rhs)(tree.pos), isStat) - case js.Closure(arrow, captureParams, params, restParam, body, captureValues) => - js.Closure(arrow, captureParams, params, restParam, body, - captureValues.map(transformExpr))(tree.pos) + newMutable(name.name, mutable), rhs)(tree.pos)) case _ => - super.transform(tree, isStat) + super.transform(tree) } } - val newBody = body.map( - b => transformer.transform(b, isStat = resultType == jstpe.NoType)) + val newBody = transformer.transformTreeOpt(body) js.MethodDef(flags, methodName, originalName, newParams, resultType, newBody)(methodDef.optimizerHints, Unversioned)(methodDef.pos) } @@ -2065,29 +2179,25 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private def patchTypeOfParamDefs(methodDef: js.MethodDef, patches: Map[LocalName, jstpe.Type]): js.MethodDef = { - def newType(name: js.LocalIdent, oldType: jstpe.Type): jstpe.Type = - patches.getOrElse(name.name, oldType) + def newType(name: LocalName, oldType: jstpe.Type): jstpe.Type = + patches.getOrElse(name, oldType) val js.MethodDef(flags, methodName, originalName, params, resultType, body) = methodDef val newParams = for { p @ js.ParamDef(name, originalName, ptpe, mutable) <- params } yield { - js.ParamDef(name, originalName, newType(name, ptpe), mutable)(p.pos) + js.ParamDef(name, originalName, newType(name.name, ptpe), mutable)(p.pos) } - val transformer = new ir.Transformers.Transformer { - override def transform(tree: js.Tree, isStat: Boolean): js.Tree = tree match { + val transformer = new ir.Transformers.LocalScopeTransformer { + override def transform(tree: js.Tree): js.Tree = tree match { case tree @ js.VarRef(name) => js.VarRef(name)(newType(name, tree.tpe))(tree.pos) - case js.Closure(arrow, captureParams, params, restParam, body, captureValues) => - js.Closure(arrow, captureParams, params, restParam, body, - captureValues.map(transformExpr))(tree.pos) case _ => - super.transform(tree, isStat) + super.transform(tree) } } - val newBody = body.map( - b => transformer.transform(b, isStat = resultType == jstpe.NoType)) + val newBody = transformer.transformTreeOpt(body) js.MethodDef(flags, methodName, originalName, newParams, resultType, newBody)(methodDef.optimizerHints, Unversioned)(methodDef.pos) } @@ -2119,7 +2229,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) optimizerHints: OptimizerHints): js.MethodDef = { implicit val pos = tree.pos - val bodyIsStat = resultIRType == jstpe.NoType + val bodyIsStat = resultIRType == jstpe.VoidType def genBodyWithinReturnableScope(): js.Tree = tree match { case Block( @@ -2163,7 +2273,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val innerBody = { withScopedVars( - thisLocalVarIdent := Some(thisLocalIdent) + thisLocalVarName := Some(thisLocalIdent.name) ) { js.Block(otherStats.map(genStat) :+ ( if (bodyIsStat) genStat(rhs) @@ -2188,50 +2298,17 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) isJSFunctionDef(currentClassSym)) { val flags = js.MemberFlags.empty.withNamespace(namespace) val body = { - def genAsUnaryOp(op: js.UnaryOp.Code): js.Tree = - js.UnaryOp(op, genThis()) - def genAsBinaryOp(op: js.BinaryOp.Code): js.Tree = - js.BinaryOp(op, genThis(), jsParams.head.ref) - def genAsBinaryOpRhsNotNull(op: js.BinaryOp.Code): js.Tree = - js.BinaryOp(op, genThis(), js.UnaryOp(js.UnaryOp.CheckNotNull, jsParams.head.ref)) - - if (currentClassSym.get == HackedStringClass) { - /* Hijack the bodies of String.length and String.charAt and replace - * them with String_length and String_charAt operations, respectively. - */ - methodName.name match { - case `lengthMethodName` => genAsUnaryOp(js.UnaryOp.String_length) - case `charAtMethodName` => genAsBinaryOp(js.BinaryOp.String_charAt) - case _ => genBody() - } - } else if (currentClassSym.get == ClassClass) { - // Similar, for the Class_x operations - methodName.name match { - case `getNameMethodName` => genAsUnaryOp(js.UnaryOp.Class_name) - case `isPrimitiveMethodName` => genAsUnaryOp(js.UnaryOp.Class_isPrimitive) - case `isInterfaceMethodName` => genAsUnaryOp(js.UnaryOp.Class_isInterface) - case `isArrayMethodName` => genAsUnaryOp(js.UnaryOp.Class_isArray) - case `getComponentTypeMethodName` => genAsUnaryOp(js.UnaryOp.Class_componentType) - case `getSuperclassMethodName` => genAsUnaryOp(js.UnaryOp.Class_superClass) - - case `isInstanceMethodName` => genAsBinaryOp(js.BinaryOp.Class_isInstance) - case `isAssignableFromMethodName` => genAsBinaryOpRhsNotNull(js.BinaryOp.Class_isAssignableFrom) - case `castMethodName` => genAsBinaryOp(js.BinaryOp.Class_cast) - - case _ => genBody() - } - } else if (currentClassSym.get == JavaLangReflectArrayModClass) { - methodName.name match { - case `arrayNewInstanceMethodName` => - val List(jlClassParam, lengthParam) = jsParams - js.BinaryOp(js.BinaryOp.Class_newArray, - js.UnaryOp(js.UnaryOp.CheckNotNull, jlClassParam.ref), - lengthParam.ref) - case _ => + val classOwner = currentClassSym.owner + if (classOwner != JavaLangPackageClass && classOwner.owner != JavaLangPackageClass) { + // Fast path; it cannot be any of the special methods of the javalib + genBody() + } else { + JavalibMethodsWithOpBody.get((encodeClassName(currentClassSym), methodName.name)) match { + case None => genBody() + case Some(javalibOpBody) => + javalibOpBody.generate(genThis(), jsParams.map(_.ref)) } - } else { - genBody() } } js.MethodDef(flags, methodName, originalName, jsParams, resultIRType, @@ -2242,7 +2319,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val thisLocalIdent = freshLocalIdent("this") withScopedVars( - thisLocalVarIdent := Some(thisLocalIdent) + thisLocalVarName := Some(thisLocalIdent.name) ) { val staticNamespace = if (namespace.isPrivate) js.MemberNamespace.PrivateStatic @@ -2299,7 +2376,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) tree match { case js.Block(stats :+ expr) => js.Block(stats :+ exprToStat(expr)) - case _:js.Literal | _:js.This | _:js.VarRef => + case _:js.Literal | _:js.VarRef => js.Skip() case _ => tree @@ -2310,8 +2387,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) */ def genExpr(tree: Tree): js.Tree = { val result = genStatOrExpr(tree, isStat = false) - assert(result.tpe != jstpe.NoType, - s"genExpr($tree) returned a tree with type NoType at pos ${tree.pos}") + assert(result.tpe != jstpe.VoidType, + s"genExpr($tree) returned a tree with type VoidType at pos ${tree.pos}") result } @@ -2388,7 +2465,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case tree @ If(cond, thenp, elsep) => def default: js.Tree = { val tpe = - if (isStat) jstpe.NoType + if (isStat) jstpe.VoidType else toIRType(tree.tpe) js.If(genExpr(cond), genStatOrExpr(thenp, isStat), @@ -2429,10 +2506,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } case Return(expr) => - js.Return(toIRType(expr.tpe) match { - case jstpe.NoType => js.Block(genStat(expr), js.Undefined()) - case _ => genExpr(expr) - }, getEnclosingReturnLabel()) + js.Return( + genStatOrExpr(expr, isStat = toIRType(expr.tpe) == jstpe.VoidType), + getEnclosingReturnLabel()) case t: Try => genTry(t, isStat) @@ -2442,9 +2518,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) ex match { case js.New(cls, _, _) if cls != JavaScriptExceptionClassName => // Common case where ex is neither null nor a js.JavaScriptException - js.Throw(ex) + js.UnaryOp(js.UnaryOp.Throw, ex) case _ => - js.Throw(js.UnwrapFromThrowable(ex)) + js.UnaryOp(js.UnaryOp.Throw, + js.UnaryOp(js.UnaryOp.UnwrapFromThrowable, js.UnaryOp(js.UnaryOp.CheckNotNull, ex))) } /* !!! Copy-pasted from `CleanUp.scala` upstream and simplified with @@ -2654,7 +2731,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case _ => mutatedLocalVars += sym js.Assign( - js.VarRef(encodeLocalSym(sym))(toIRType(sym.tpe)), + js.VarRef(encodeLocalSymName(sym))(toIRType(sym.tpe)), genRhs) } @@ -2704,15 +2781,15 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * is one. */ private def genThis()(implicit pos: Position): js.Tree = { - thisLocalVarIdent.fold[js.Tree] { - if (tryingToGenMethodAsJSFunction) { - throw new CancelGenMethodAsJSFunction( - "Trying to generate `this` inside the body") + thisLocalVarName.fold[js.Tree] { + if (isJSFunctionDef(currentClassSym)) { + abort( + "Unexpected `this` reference inside the body of a JS function class: " + + currentClassSym.fullName) } js.This()(currentThisType) - } { thisLocalIdent => - // .copy() to get the correct position - js.VarRef(thisLocalIdent.copy())(currentThisTypeNullable) + } { thisLocalName => + js.VarRef(thisLocalName)(currentThisTypeNullable) } } @@ -2746,7 +2823,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * are transformed into * {{{ * ...labelParams = ...args; - * return@labelName (void 0) + * return@labelName; * }}} * * This is always correct, so it can handle arbitrary labels and jumps @@ -2762,8 +2839,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val labelParamSyms = tree.params.map(_.symbol) val info = new EnclosingLabelDefInfoWithResultAsAssigns(labelParamSyms) - val labelIdent = encodeLabelSym(sym) - val labelName = labelIdent.name + val labelName = encodeLabelSym(sym) val transformedRhs = withScopedVars( enclosingLabelDefInfos := enclosingLabelDefInfos.get + (sym -> info) @@ -2778,7 +2854,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) */ object ReturnFromThisLabel { def unapply(tree: js.Return): Option[js.Tree] = { - if (tree.label.name == labelName) Some(exprToStat(tree.expr)) + if (tree.label == labelName) Some(exprToStat(tree.expr)) else None } } @@ -2787,7 +2863,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (transformedRhs.tpe == jstpe.NothingType) { // In this case, we do not need the outer block label js.While(js.BooleanLiteral(true), { - js.Labeled(labelIdent, jstpe.NoType, { + js.Labeled(labelName, jstpe.VoidType, { transformedRhs match { // Eliminate a trailing return@lab case js.Block(stats :+ ReturnFromThisLabel(exprAsStat)) => @@ -2799,17 +2875,17 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) }) } else { // When all else has failed, we need the full machinery - val blockLabelIdent = freshLabelIdent("block") + val blockLabelName = freshLabelName("block") val bodyType = - if (isStat) jstpe.NoType + if (isStat) jstpe.VoidType else toIRType(tree.tpe) - js.Labeled(blockLabelIdent, bodyType, { + js.Labeled(blockLabelName, bodyType, { js.While(js.BooleanLiteral(true), { - js.Labeled(labelIdent, jstpe.NoType, { + js.Labeled(labelName, jstpe.VoidType, { if (isStat) - js.Block(transformedRhs, js.Return(js.Undefined(), blockLabelIdent)) + js.Block(transformedRhs, js.Return(js.Skip(), blockLabelName)) else - js.Return(transformedRhs, blockLabelIdent) + js.Return(transformedRhs, blockLabelName) }) }) }) @@ -2921,7 +2997,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val blockAST = genStatOrExpr(block, isStat) val resultType = - if (isStat) jstpe.NoType + if (isStat) jstpe.VoidType else toIRType(tree.tpe) val handled = @@ -2965,7 +3041,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) implicit pos: Position): js.Tree = { val exceptIdent = freshLocalIdent("e") - val origExceptVar = js.VarRef(exceptIdent)(jstpe.AnyType) + val origExceptVar = js.VarRef(exceptIdent.name)(jstpe.AnyType) val mightCatchJavaScriptException = catches.exists { caseDef => caseDef.pat match { @@ -2980,13 +3056,14 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val (exceptValDef, exceptVar) = if (mightCatchJavaScriptException) { val valDef = js.VarDef(freshLocalIdent("e"), NoOriginalName, - encodeClassType(ThrowableClass), mutable = false, js.WrapAsThrowable(origExceptVar)) + encodeClassType(ThrowableClass), mutable = false, + js.UnaryOp(js.UnaryOp.WrapAsThrowable, origExceptVar)) (valDef, valDef.ref) } else { (js.Skip(), origExceptVar) } - val elseHandler: js.Tree = js.Throw(origExceptVar) + val elseHandler: js.Tree = js.UnaryOp(js.UnaryOp.Throw, origExceptVar) val handler = catches.foldRight(elseHandler) { (caseDef, elsep) => implicit val pos = caseDef.pos @@ -3153,7 +3230,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val r = toIRType(to) def isValueType(tpe: jstpe.Type): Boolean = tpe match { - case jstpe.NoType | jstpe.BooleanType | jstpe.CharType | + case jstpe.VoidType | jstpe.BooleanType | jstpe.CharType | jstpe.ByteType | jstpe.ShortType | jstpe.IntType | jstpe.LongType | jstpe.FloatType | jstpe.DoubleType => true @@ -3195,7 +3272,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private def genThrowClassCastException()(implicit pos: Position): js.Tree = { val ctor = ClassCastExceptionClass.info.member( nme.CONSTRUCTOR).suchThat(_.tpe.params.isEmpty) - js.Throw(genNew(ClassCastExceptionClass, ctor, Nil)) + js.UnaryOp(js.UnaryOp.Throw, genNew(ClassCastExceptionClass, ctor, Nil)) } /** Gen JS code for a super call, of the form Class.super[mix].fun(args). @@ -3241,10 +3318,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } /** Gen JS code for a constructor call (new). + * * Further refined into: - * * new String(...) * * new of a hijacked boxed class - * * new of an anonymous function class that was recorded as JS function + * * new of a JS function class * * new of a JS class * * new Array * * regular new @@ -3264,13 +3341,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } else if (isJSFunctionDef(clsSym)) { val classDef = consumeLazilyGeneratedAnonClass(clsSym) genJSFunction(classDef, args.map(genExpr)) - } else if (clsSym.isAnonymousFunction) { - val classDef = consumeLazilyGeneratedAnonClass(clsSym) - tryGenAnonFunctionClass(classDef, args.map(genExpr)).getOrElse { - // Cannot optimize anonymous function class. Generate full class. - generatedClasses += nestedGenerateClass(clsSym)(genClass(classDef)) -> clsSym.pos - genNew(clsSym, ctor, genActualArgs(ctor, args)) - } } else if (isJSType(clsSym)) { genPrimitiveJSNew(tree) } else { @@ -3281,6 +3351,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) genNewArray(arr, args.map(genExpr)) case prim: jstpe.PrimRef => abort(s"unexpected primitive type $prim in New at $pos") + case typeRef: jstpe.TransientTypeRef => + abort(s"unexpected special type ref $typeRef in New at $pos") } } } @@ -3318,7 +3390,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val paramSyms = info.paramSyms assertArgCountMatches(paramSyms.size) - val jump = js.Return(js.Undefined(), labelIdent) + val jump = js.Return(js.Skip(), labelIdent) if (args.isEmpty) { // fast path, applicable notably to loops and case labels @@ -3358,7 +3430,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) List.newBuilder[(js.VarRef, jstpe.Type, js.LocalIdent, js.Tree)] for ((formalArgSym, arg) <- targetSyms.zip(values)) { - val formalArg = encodeLocalSym(formalArgSym) + val formalArgName = encodeLocalSymName(formalArgSym) val actualArg = genExpr(arg) /* #3267 The formal argument representing the special `this` of a @@ -3383,13 +3455,13 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) else actualArg actualArg match { - case js.VarRef(`formalArg`) => + case js.VarRef(`formalArgName`) => // This is trivial assignment, we don't need it case _ => mutatedLocalVars += formalArgSym - quadruplets += ((js.VarRef(formalArg)(tpe), tpe, - freshLocalIdent(formalArg.name.withPrefix("temp$")), + quadruplets += ((js.VarRef(formalArgName)(tpe), tpe, + freshLocalIdent(formalArgName.withPrefix("temp$")), fixedActualArg)) } } @@ -3410,7 +3482,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) yield js.VarDef(tempArg, NoOriginalName, argType, mutable = false, actualArg) val trueAssignments = for ((formalArg, argType, tempArg, _) <- quadruplets) - yield js.Assign(formalArg, js.VarRef(tempArg)(argType)) + yield js.Assign(formalArg, js.VarRef(tempArg.name)(argType)) tempAssignments ::: trueAssignments } } @@ -3445,7 +3517,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) genApplyJSClassMethod(genExpr(receiver), sym, genActualArgs(sym, args)) } else if (sym.hasAnnotation(JSNativeAnnotation)) { genJSNativeMemberCall(tree, isStat) - } else if (sym.isStaticMember) { + } else if (compileAsStaticMethod(sym)) { if (sym.isMixinConstructor) { /* Do not emit a call to the $init$ method of JS traits. * This exception is necessary because optional JS fields cause the @@ -3496,7 +3568,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) .withNoinline(noinline) val methodIdent = encodeMethodSym(method) val resultType = - if (method.isClassConstructor) jstpe.NoType + if (method.isClassConstructor) jstpe.VoidType else toIRType(method.tpe.resultType) js.ApplyStatically(flags, receiver, encodeClassName(method.owner), methodIdent, arguments)(resultType) @@ -3757,101 +3829,63 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * true `int`s can reach the back-end, as asserted by the String-switch * transformation in `cleanup`. Therefore, we do not adapt, preserving * the `string`s and `null`s that come out of the pattern matching in - * Scala 2.13.2+. + * Scala 2.13.x. */ val genSelector = genExpr(selector) val resultType = - if (isStat) jstpe.NoType + if (isStat) jstpe.VoidType else toIRType(tree.tpe) - val defaultLabelSym = cases.collectFirst { + val optDefaultLabelSymAndInfo = cases.collectFirst { case CaseDef(Ident(nme.WILDCARD), EmptyTree, body @ LabelDef(_, Nil, rhs)) if hasSynthCaseSymbol(body) => - body.symbol - }.getOrElse(NoSymbol) + body.symbol -> new EnclosingLabelDefInfoWithResultAsAssigns(Nil) + } var clauses: List[(List[js.MatchableLiteral], js.Tree)] = Nil var optElseClause: Option[js.Tree] = None - var optElseClauseLabel: Option[js.LabelIdent] = None - - def genJumpToElseClause(implicit pos: ir.Position): js.Tree = { - if (optElseClauseLabel.isEmpty) - optElseClauseLabel = Some(freshLabelIdent("default")) - js.Return(js.Undefined(), optElseClauseLabel.get) - } - - for (caze @ CaseDef(pat, guard, body) <- cases) { - assert(guard == EmptyTree, s"found a case guard at ${caze.pos}") - - def genBody(body: Tree): js.Tree = body match { - case app @ Apply(_, Nil) if app.symbol == defaultLabelSym => - genJumpToElseClause - case Block(List(app @ Apply(_, Nil)), _) if app.symbol == defaultLabelSym => - genJumpToElseClause - - case If(cond, thenp, elsep) => - js.If(genExpr(cond), genBody(thenp), genBody(elsep))( - resultType)(body.pos) - - /* For #1955. If we receive a tree with the shape - * if (cond) { - * thenp - * } else { - * elsep - * } - * scala.runtime.BoxedUnit.UNIT - * we rewrite it as - * if (cond) { - * thenp - * scala.runtime.BoxedUnit.UNIT - * } else { - * elsep - * scala.runtime.BoxedUnit.UNIT - * } - * so that it fits the shape of if/elses we can deal with. - */ - case Block(List(If(cond, thenp, elsep)), s: Select) - if s.symbol == definitions.BoxedUnit_UNIT => - val newThenp = Block(thenp, s).setType(s.tpe).setPos(thenp.pos) - val newElsep = Block(elsep, s).setType(s.tpe).setPos(elsep.pos) - js.If(genExpr(cond), genBody(newThenp), genBody(newElsep))( - resultType)(body.pos) - case _ => + withScopedVars( + enclosingLabelDefInfos := enclosingLabelDefInfos.get ++ optDefaultLabelSymAndInfo.toList + ) { + for (caze @ CaseDef(pat, guard, body) <- cases) { + assert(guard == EmptyTree, s"found a case guard at ${caze.pos}") + + def genBody(body: Tree): js.Tree = genStatOrExpr(body, isStat) - } - def invalidCase(tree: Tree): Nothing = - abort(s"Invalid case in alternative in switch-like pattern match: $tree at: ${tree.pos}") + def invalidCase(tree: Tree): Nothing = + abort(s"Invalid case in alternative in switch-like pattern match: $tree at: ${tree.pos}") - def genMatchableLiteral(tree: Literal): js.MatchableLiteral = { - genExpr(tree) match { - case matchableLiteral: js.MatchableLiteral => matchableLiteral - case otherExpr => invalidCase(tree) + def genMatchableLiteral(tree: Literal): js.MatchableLiteral = { + genExpr(tree) match { + case matchableLiteral: js.MatchableLiteral => matchableLiteral + case otherExpr => invalidCase(tree) + } } - } - pat match { - case lit: Literal => - clauses = (List(genMatchableLiteral(lit)), genBody(body)) :: clauses - case Ident(nme.WILDCARD) => - optElseClause = Some(body match { - case LabelDef(_, Nil, rhs) if hasSynthCaseSymbol(body) => - genBody(rhs) - case _ => - genBody(body) - }) - case Alternative(alts) => - val genAlts = { - alts map { - case lit: Literal => genMatchableLiteral(lit) - case _ => invalidCase(tree) + pat match { + case lit: Literal => + clauses = (List(genMatchableLiteral(lit)), genBody(body)) :: clauses + case Ident(nme.WILDCARD) => + optElseClause = Some(body match { + case LabelDef(_, Nil, rhs) if hasSynthCaseSymbol(body) => + genBody(rhs) + case _ => + genBody(body) + }) + case Alternative(alts) => + val genAlts = { + alts map { + case lit: Literal => genMatchableLiteral(lit) + case _ => invalidCase(tree) + } } - } - clauses = (genAlts, genBody(body)) :: clauses - case _ => - invalidCase(tree) + clauses = (genAlts, genBody(body)) :: clauses + case _ => + invalidCase(tree) + } } } @@ -3894,23 +3928,23 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } - optElseClauseLabel.fold[js.Tree] { - buildMatch(clauses.reverse, elseClause, resultType) - } { elseClauseLabel => - val matchResultLabel = freshLabelIdent("matchResult") - val patchedClauses = for ((alts, body) <- clauses) yield { - implicit val pos = body.pos - val newBody = - if (isStat) js.Block(body, js.Return(js.Undefined(), matchResultLabel)) - else js.Return(body, matchResultLabel) - (alts, newBody) - } - js.Labeled(matchResultLabel, resultType, js.Block(List( - js.Labeled(elseClauseLabel, jstpe.NoType, { - buildMatch(patchedClauses.reverse, js.Skip(), jstpe.NoType) + optDefaultLabelSymAndInfo match { + case Some((defaultLabelSym, defaultLabelInfo)) if defaultLabelInfo.generatedReturns > 0 => + val matchResultLabel = freshLabelName("matchResult") + val patchedClauses = for ((alts, body) <- clauses) yield { + implicit val pos = body.pos + val newBody = js.Return(body, matchResultLabel) + (alts, newBody) + } + js.Labeled(matchResultLabel, resultType, js.Block(List( + js.Labeled(encodeLabelSym(defaultLabelSym), jstpe.VoidType, { + buildMatch(patchedClauses.reverse, js.Skip(), jstpe.VoidType) }), elseClause - ))) + ))) + + case _ => + buildMatch(clauses.reverse, elseClause, resultType) } } @@ -4004,12 +4038,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val translatedMatch = genTranslatedMatch(cases, matchEnd) val genMore = genBlockWithCaseLabelDefs(more, isStat) val label = getEnclosingReturnLabel() - if (translatedMatch.tpe == jstpe.NoType) { - // Could not actually reproduce this, but better be safe than sorry - translatedMatch :: js.Return(js.Undefined(), label) :: genMore - } else { - js.Return(translatedMatch, label) :: genMore - } + js.Return(translatedMatch, label) :: genMore // Otherwise, there is no matchEnd, only consecutive cases case Nil => @@ -4088,7 +4117,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) tree match { case If(cond, thenp, elsep) => js.If(genExpr(cond), genCaseBody(thenp), genCaseBody(elsep))( - jstpe.NoType) + jstpe.VoidType) case Block(stats, Literal(Constant(()))) => // Generated a lot by the async transform @@ -4143,7 +4172,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) def genMatchEndBody(): js.Tree = { genStatOrExpr(matchEndBody, - isStat = toIRType(matchEndBody.tpe) == jstpe.NoType) + isStat = toIRType(matchEndBody.tpe) == jstpe.VoidType) } matchEnd.params match { @@ -4197,7 +4226,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) ) { genTranslatedCases } - val optimized = genOptimizedMatchEndLabeled(labelIdent, jstpe.NoType, + val optimized = genOptimizedMatchEndLabeled(labelIdent, jstpe.VoidType, translatedCases, info.generatedReturns) js.Block(varDefs ::: optimized :: genMatchEndBody() :: Nil) } @@ -4245,12 +4274,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * all jumps to case labels are already caught upstream by `genCaseBody()` * inside `genTranslatedMatch()`. */ - private def genOptimizedCaseLabeled(label: js.LabelIdent, + private def genOptimizedCaseLabeled(label: LabelName, translatedBody: js.Tree, returnCount: Int)( implicit pos: Position): js.Tree = { def default: js.Tree = - js.Labeled(label, jstpe.NoType, translatedBody) + js.Labeled(label, jstpe.VoidType, translatedBody) if (returnCount == 0) { translatedBody @@ -4260,21 +4289,21 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) translatedBody match { case js.Block(stats) => val (stats1, testAndStats2) = stats.span { - case js.If(_, js.Return(js.Undefined(), `label`), js.Skip()) => + case js.If(_, js.Return(_, `label`), js.Skip()) => false case _ => true } testAndStats2 match { - case js.If(cond, _, _) :: stats2 => + case js.If(cond, js.Return(returnedValue, _), _) :: stats2 => val notCond = cond match { case js.UnaryOp(js.UnaryOp.Boolean_!, notCond) => notCond case _ => js.UnaryOp(js.UnaryOp.Boolean_!, cond) } - js.Block(stats1 :+ js.If(notCond, js.Block(stats2), js.Skip())(jstpe.NoType)) + js.Block(stats1 :+ js.If(notCond, js.Block(stats2), returnedValue)(jstpe.VoidType)) case _ :: _ => throw new AssertionError("unreachable code") @@ -4302,7 +4331,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * !!! There is quite of bit of code duplication with * OptimizerCore.tryOptimizePatternMatch. */ - def genOptimizedMatchEndLabeled(label: js.LabelIdent, tpe: jstpe.Type, + def genOptimizedMatchEndLabeled(label: LabelName, tpe: jstpe.Type, translatedCases: List[js.Tree], returnCount: Int)( implicit pos: Position): js.Tree = { def default: js.Tree = @@ -4770,7 +4799,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.Assign(genSelect(fun.tpe.paramTypes(1)), arguments(1)) } else { // length of the array - js.ArrayLength(arrayValue) + js.UnaryOp(js.UnaryOp.Array_length, + js.UnaryOp(js.UnaryOp.CheckNotNull, arrayValue)) } } @@ -4783,8 +4813,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val newReceiver = genExpr(receiver) val newArg = genStatOrExpr(arg, isStat) newReceiver match { - case js.This() => - // common case for which there is no side-effect nor NPE + case newReceiver: js.VarRef if !newReceiver.tpe.isNullable => + // common case (notably for `this`) for which there is no side-effect nor NPE newArg case _ => js.Block( @@ -4881,7 +4911,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val callTrgIdent = freshLocalIdent() val callTrgVarDef = js.VarDef(callTrgIdent, NoOriginalName, receiverType, mutable = false, genExpr(receiver)) - val callTrg = js.VarRef(callTrgIdent)(receiverType) + val callTrg = js.VarRef(callTrgIdent.name)(receiverType) val arguments = args zip sym.tpe.params map { case (arg, param) => /* No need for enteringPosterasure, because value classes are not @@ -5086,11 +5116,33 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } + /** Adapt boxes on a tree from and to the given types after posterasure. + * + * @param expr + * Tree to be adapted. + * @param fromTpeEnteringPosterasure + * The type of `expr` as it was entering the posterasure phase. + * @param toTpeEnteringPosterausre + * The type of the adapted tree as it would be entering the posterasure phase. + */ + def adaptBoxes(expr: js.Tree, fromTpeEnteringPosterasure: Type, + toTpeEnteringPosterasure: Type)( + implicit pos: Position): js.Tree = { + if (fromTpeEnteringPosterasure =:= toTpeEnteringPosterasure) { + expr + } else { + /* Upcast to `Any` then downcast to `toTpe`. This is not very smart. + * We rely on the optimizer to get rid of unnecessary casts. + */ + fromAny(ensureBoxed(expr, fromTpeEnteringPosterasure), toTpeEnteringPosterasure) + } + } + /** Gen a boxing operation (tpe is the primitive type) */ def makePrimitiveBox(expr: js.Tree, tpe: Type)( implicit pos: Position): js.Tree = { toIRType(tpe) match { - case jstpe.NoType => // for JS interop cases + case jstpe.VoidType => // for JS interop cases js.Block(expr, js.Undefined()) case jstpe.BooleanType | jstpe.CharType | jstpe.ByteType | jstpe.ShortType | jstpe.IntType | jstpe.LongType | jstpe.FloatType | @@ -5105,8 +5157,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) def makePrimitiveUnbox(expr: js.Tree, tpe: Type)( implicit pos: Position): js.Tree = { toIRType(tpe) match { - case jstpe.NoType => expr // for JS interop cases - case irTpe => js.AsInstanceOf(expr, irTpe) + case jstpe.VoidType => expr // for JS interop cases + case irTpe => js.AsInstanceOf(expr, irTpe) } } @@ -5202,15 +5254,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) genStatOrExpr(args(1), isStat) } - case LINKING_INFO => - // runtime.linkingInfo - js.JSLinkingInfo() - - case IDENTITY_HASH_CODE => - // runtime.identityHashCode(arg) - val arg = genArgs1 - js.IdentityHashCode(arg) - case DEBUGGER => // js.special.debugger() js.Debugger() @@ -5255,6 +5298,44 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // js.import.meta js.JSImportMeta() + case JS_ASYNC => + // js.async(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(_, Apply(target, _))) = args.head + methodsAllowingJSAwait += target.symbol + val genStats = stats.map(genStat(_)) + val asyncExpr = genAnonFunction(fun) match { + case js.NewLambda(_, closure: js.Closure) + if closure.params.isEmpty && closure.resultType == jstpe.AnyType => + val newFlags = closure.flags.withTyped(false).withAsync(true) + js.JSFunctionApply(closure.copy(flags = newFlags), Nil) + case other => + abort(s"Unexpected tree generated for the Function0 argument to js.async at $pos: $other") + } + js.Block(genStats, asyncExpr) + + case JS_AWAIT => + // js.await(arg)(permit) + val (arg, permitValue) = genArgs2 + if (!methodsAllowingJSAwait.contains(currentMethodSym)) { + // This is an orphan await + if (!(args(1).tpe <:< WasmJSPI_allowOrphanJSAwaitModuleClass.toTypeConstructor)) { + reporter.error(pos, + "Illegal use of js.await().\n" + + "It can only be used inside a js.async {...} block, without any lambda,\n" + + "by-name argument or nested method in-between.\n" + + "If you compile for WebAssembly, you can allow arbitrary js.await()\n" + + "calls by adding the following import:\n" + + "import scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait") + } + } + /* In theory we should evaluate `permit` after `arg` but before the `JSAwait`. + * It *should* always be side-effect-free, though, so we just discard it. + */ + js.JSAwait(arg) + case DYNAMIC_IMPORT => assert(args.size == 1, s"Expected exactly 1 argument for JS primitive $code but got " + @@ -5336,7 +5417,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val fVarDef = js.VarDef(freshLocalIdent("f"), NoOriginalName, jstpe.AnyType, mutable = false, arg2) val keyVarIdent = freshLocalIdent("key") - val keyVarRef = js.VarRef(keyVarIdent)(jstpe.AnyType) + val keyVarRef = js.VarRef(keyVarIdent.name)(jstpe.AnyType) js.Block( objVarDef, fVarDef, @@ -5346,7 +5427,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case JS_THROW => // js.special.throw(arg) - js.Throw(genArgs1) + js.UnaryOp(js.UnaryOp.Throw, genArgs1) case JS_TRY_CATCH => /* js.special.tryCatch(arg1, arg2) @@ -5371,7 +5452,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val handlerVarDef = js.VarDef(freshLocalIdent("handler"), NoOriginalName, jstpe.AnyType, mutable = false, arg2) val exceptionVarIdent = freshLocalIdent("e") - val exceptionVarRef = js.VarRef(exceptionVarIdent)(jstpe.AnyType) + val exceptionVarRef = js.VarRef(exceptionVarIdent.name)(jstpe.AnyType) js.Block( bodyVarDef, handlerVarDef, @@ -5385,11 +5466,115 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case WRAP_AS_THROWABLE => // js.special.wrapAsThrowable(arg) - js.WrapAsThrowable(genArgs1) + js.UnaryOp(js.UnaryOp.WrapAsThrowable, genArgs1) case UNWRAP_FROM_THROWABLE => // js.special.unwrapFromThrowable(arg) - js.UnwrapFromThrowable(genArgs1) + js.UnaryOp(js.UnaryOp.UnwrapFromThrowable, + js.UnaryOp(js.UnaryOp.CheckNotNull, genArgs1)) + + case LINKTIME_IF => + // LinkingInfo.linkTimeIf(cond, thenp, elsep) + val cond = genLinkTimeExpr(args(0)) + val thenp = genExpr(args(1)) + val elsep = genExpr(args(2)) + val tpe = + if (isStat) jstpe.VoidType + else toIRType(tree.tpe) + js.LinkTimeIf(cond, thenp, elsep)(tpe) + + case LINKTIME_PROPERTY => + // LinkingInfo.linkTimePropertyXXX("...") + val arg = genArgs1 + val tpe: jstpe.Type = toIRType(tree.tpe) match { + case jstpe.ClassType(jswkn.BoxedStringClass, _) => jstpe.StringType + case irType => irType + } + arg match { + case js.StringLiteral(name) => + js.LinkTimeProperty(name)(tpe) + case _ => + reporter.error(args.head.pos, + "The argument of linkTimePropertyXXX must be a String literal: \"...\"") + js.LinkTimeProperty("erroneous")(tpe) + } + } + } + + private def genLinkTimeExpr(tree: Tree): js.Tree = { + import scalaPrimitives._ + + implicit val pos = tree.pos + + def invalid(): js.Tree = { + reporter.error(tree.pos, + "Illegal expression in the condition of a linkTimeIf. " + + "Valid expressions are: boolean and int primitives; " + + "references to link-time properties; " + + "primitive operations on booleans; " + + "and comparisons on ints.") + js.BooleanLiteral(false) + } + + tree match { + case Literal(c) => + c.tag match { + case BooleanTag => js.BooleanLiteral(c.booleanValue) + case IntTag => js.IntLiteral(c.intValue) + case _ => invalid() + } + + case Apply(fun @ Select(receiver, _), args) => + fun.symbol.getAnnotation(LinkTimePropertyAnnotation) match { + case Some(annotation) => + val propName = annotation.constantAtIndex(0).get.stringValue + js.LinkTimeProperty(propName)(toIRType(tree.tpe)) + + case None if isPrimitive(fun.symbol) => + val code = getPrimitive(fun.symbol) + + def genLhs: js.Tree = genLinkTimeExpr(receiver) + def genRhs: js.Tree = genLinkTimeExpr(args.head) + + def unaryOp(op: js.UnaryOp.Code): js.Tree = + js.UnaryOp(op, genLhs) + def binaryOp(op: js.BinaryOp.Code): js.Tree = + js.BinaryOp(op, genLhs, genRhs) + + toIRType(receiver.tpe) match { + case jstpe.BooleanType => + (code: @switch) match { + case ZNOT => unaryOp(js.UnaryOp.Boolean_!) + case EQ => binaryOp(js.BinaryOp.Boolean_==) + case NE | XOR => binaryOp(js.BinaryOp.Boolean_!=) + case OR => binaryOp(js.BinaryOp.Boolean_|) + case AND => binaryOp(js.BinaryOp.Boolean_&) + case ZOR => js.LinkTimeIf(genLhs, js.BooleanLiteral(true), genRhs)(jstpe.BooleanType) + case ZAND => js.LinkTimeIf(genLhs, genRhs, js.BooleanLiteral(false))(jstpe.BooleanType) + case _ => invalid() + } + + case jstpe.IntType => + (code: @switch) match { + case EQ => binaryOp(js.BinaryOp.Int_==) + case NE => binaryOp(js.BinaryOp.Int_!=) + case LT => binaryOp(js.BinaryOp.Int_<) + case LE => binaryOp(js.BinaryOp.Int_<=) + case GT => binaryOp(js.BinaryOp.Int_>) + case GE => binaryOp(js.BinaryOp.Int_>=) + case _ => invalid() + } + + case _ => + invalid() + } + + case None => // if !isPrimitive + invalid() + } + + case _ => + invalid() } } @@ -5501,8 +5686,16 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) def genSelectGet(propName: js.Tree): js.Tree = genSuperReference(propName) - def genSelectSet(propName: js.Tree, value: js.Tree): js.Tree = - js.Assign(genSuperReference(propName), value) + def genSelectSet(propName: js.Tree, value: js.Tree): js.Tree = { + val lhs = genSuperReference(propName) + lhs match { + case js.JSGlobalRef(js.JSGlobalRef.FileLevelThis) => + reporter.error(pos, + "Illegal assignment to global this.") + case _ => + } + js.Assign(lhs, value) + } def genCall(methodName: js.Tree, args: List[js.TreeOrJSSpread]): js.Tree = { @@ -5960,78 +6153,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Synthesizers for JS functions ------------------------------------------- - /** Try and generate JS code for an anonymous function class. - * - * Returns Some() if the class could be rewritten that way, None - * otherwise. - * - * We make the following assumptions on the form of such classes: - * - It is an anonymous function - * - Includes being anonymous, final, and having exactly one constructor - * - It is not a PartialFunction - * - It has no field other than param accessors - * - It has exactly one constructor - * - It has exactly one non-bridge method apply if it is not specialized, - * or a method apply$...$sp and a forwarder apply if it is specialized. - * - As a precaution: it is synthetic - * - * From a class looking like this: - * - * final class (outer, capture1, ..., captureM) extends AbstractionFunctionN[...] { - * def apply(param1, ..., paramN) = { - * - * } - * } - * new (o, c1, ..., cM) - * - * we generate a function: - * - * lambda[notype]( - * outer, capture1, ..., captureM, param1, ..., paramN) { - * - * } - * - * so that, at instantiation point, we can write: - * - * new AnonFunctionN(function) - * - * the latter tree is returned in case of success. - * - * Trickier things apply when the function is specialized. - */ - private def tryGenAnonFunctionClass(cd: ClassDef, - capturedArgs: List[js.Tree]): Option[js.Tree] = { - // scalastyle:off return - implicit val pos = cd.pos - val sym = cd.symbol - assert(sym.isAnonymousFunction, - s"tryGenAndRecordAnonFunctionClass called with non-anonymous function $cd") - - if (!sym.superClass.fullName.startsWith("scala.runtime.AbstractFunction")) { - /* This is an anonymous class for a non-LMF capable SAM in 2.12. - * We must not rewrite it, as it would then not inherit from the - * appropriate parent class and/or interface. - */ - None - } else { - nestedGenerateClass(sym) { - val (functionBase, arity) = - tryGenAnonFunctionClassGeneric(cd, capturedArgs)(_ => return None) - - Some(genJSFunctionToScala(functionBase, arity)) - } - } - // scalastyle:on return - } - - /** Gen a conversion from a JavaScript function into a Scala function. */ - private def genJSFunctionToScala(jsFunction: js.Tree, arity: Int)( - implicit pos: Position): js.Tree = { - val clsSym = getRequiredClass("scala.scalajs.runtime.AnonFunction" + arity) - val ctor = clsSym.primaryConstructor - genNew(clsSym, ctor, List(jsFunction)) - } - /** Gen JS code for a JS function class. * * This is called when emitting a ClassDef that represents an anonymous @@ -6040,11 +6161,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * functions are not classes, we deconstruct the ClassDef, then * reconstruct it to be a genuine Closure. * - * Compared to `tryGenAnonFunctionClass()`, this function must - * always succeed, because we really cannot afford keeping them as - * anonymous classes. The good news is that it can do so, because the - * body of SAM lambdas is hoisted in the enclosing class. Hence, the - * apply() method is just a forwarder to calling that hoisted method. + * We can always do so, because the body of SAM lambdas is hoisted in the + * enclosing class. Hence, the apply() method is just a forwarder to + * calling that hoisted method. * * From a class looking like this: * @@ -6057,8 +6176,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * * we generate a function: * - * lambda[notype]( - * outer, capture1, ..., captureM, param1, ..., paramN) { + * arrow-lambda(param1, ..., paramN) { * outer.lambdaImpl(param1, ..., paramN, capture1, ..., captureM) * } */ @@ -6068,26 +6186,18 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) s"genAndRecordJSFunctionClass called with non-JS function $cd") nestedGenerateClass(sym) { - val (function, _) = tryGenAnonFunctionClassGeneric(cd, captures)(msg => - abort(s"Could not generate function for JS function: $msg")) - - function + genJSFunctionInner(cd, captures) } } - /** Code common to tryGenAndRecordAnonFunctionClass and - * genAndRecordJSFunctionClass. - */ - private def tryGenAnonFunctionClassGeneric(cd: ClassDef, - initialCapturedArgs: List[js.Tree])( - fail: (=> String) => Nothing): (js.Tree, Int) = { + /** The code of `genJSFunction` that is inside the `nestedGenerateClass` wrapper. */ + private def genJSFunctionInner(cd: ClassDef, + initialCapturedArgs: List[js.Tree]): js.Closure = { implicit val pos = cd.pos val sym = cd.symbol - // First checks - - if (sym.isSubClass(PartialFunctionClass)) - fail(s"Cannot rewrite PartialFunction $cd") + def fail(reason: String): Nothing = + abort(s"Could not generate function for JS function: $reason") // First step: find the apply method def, and collect param accessors @@ -6115,10 +6225,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (!ddsym.isPrimaryConstructor) fail(s"Non-primary constructor $ddsym in anon function $cd") } else { - val name = dd.name.toString - if (name == "apply" || (ddsym.isSpecialized && name.startsWith("apply$"))) { - if ((applyDef eq null) || ddsym.isSpecialized) + if (dd.name == nme.apply) { + if (!ddsym.isBridge) { + if (applyDef ne null) + fail(s"Found duplicate apply method $ddsym in $cd") applyDef = dd + } } else if (ddsym.hasAnnotation(JSOptionalAnnotation)) { // Ignore (this is useful for default parameters in custom JS function types) } else { @@ -6158,24 +6270,15 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Third step: emit the body of the apply method def val applyMethod = withScopedVars( - paramAccessorLocals := (paramAccessors zip ctorParamDefs).toMap, - tryingToGenMethodAsJSFunction := true + paramAccessorLocals := (paramAccessors zip ctorParamDefs).toMap ) { - try { - genMethodWithCurrentLocalNameScope(applyDef) - } catch { - case e: CancelGenMethodAsJSFunction => - fail(e.getMessage) - } + genMethodWithCurrentLocalNameScope(applyDef) } // Fourth step: patch the body to unbox parameters and box result - val hasRepeatedParam = { - sym.superClass == JSFunctionClass && // Scala functions are known not to have repeated params - enteringUncurry { - applyDef.symbol.paramss.flatten.lastOption.exists(isRepeated(_)) - } + val hasRepeatedParam = enteringUncurry { + applyDef.symbol.paramss.flatten.lastOption.exists(isRepeated(_)) } val js.MethodDef(_, _, _, params, _, body) = applyMethod @@ -6184,7 +6287,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (hasRepeatedParam) params.init else params patchFunParamsWithBoxes(applyDef.symbol, nonRepeatedParams, - useParamsBeforeLambdaLift = false) + useParamsBeforeLambdaLift = false, + fromParamTypes = nonRepeatedParams.map(_ => ObjectTpe)) } val (patchedRepeatedParam, repeatedParamLocal) = { @@ -6192,7 +6296,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * But that lowers the type to iterable. */ if (hasRepeatedParam) { - val (p, l) = genPatchedParam(params.last, genJSArrayToVarArgs(_)) + val (p, l) = genPatchedParam(params.last, genJSArrayToVarArgs(_), jstpe.AnyType) (Some(p), Some(l)) } else { (None, None) @@ -6208,8 +6312,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val ok = patchedParams.nonEmpty if (!ok) { reporter.error(pos, - "The SAM or apply method for a js.ThisFunction must have a " + - "leading non-varargs parameter") + "The apply method for a js.ThisFunction must have a leading non-varargs parameter") } ok } @@ -6224,10 +6327,11 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (isThisFunction) { val thisParam :: actualParams = patchedParams js.Closure( - arrow = false, + js.ClosureFlags.function, ctorParamDefs, actualParams, patchedRepeatedParam, + jstpe.AnyType, js.Block( js.VarDef(thisParam.name, thisParam.originalName, thisParam.ptpe, mutable = false, @@ -6235,14 +6339,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) patchedBody), capturedArgs) } else { - js.Closure(arrow = true, ctorParamDefs, patchedParams, - patchedRepeatedParam, patchedBody, capturedArgs) + js.Closure(js.ClosureFlags.arrow, ctorParamDefs, patchedParams, + patchedRepeatedParam, jstpe.AnyType, patchedBody, capturedArgs) } } - val arity = params.size - - (closure, arity) + closure } } @@ -6261,30 +6363,48 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * We identify the captures using the same method as the `delambdafy` * phase. We have an additional hack for `this`. * - * To translate them, we first construct a JS closure for the body: + * To translate them, we first construct a typed closure for the body: * {{{ - * lambda( - * _this, capture1, ..., captureM, arg1, ..., argN) { - * _this.someMethod(arg1, ..., argN, capture1, ..., captureM) + * typed-lambda<_this = this, capture1: U1 = capture1, ..., captureM: UM = captureM>( + * arg1: T1, ..., argN: TN): TR = { + * val arg1Unboxed: S1 = arg1.asInstanceOf[S1]; + * ... + * val argNUnboxed: SN = argN.asInstanceOf[SN]; + * // inlined body of `someMethod`, boxed * } * }}} * In the closure, input params are unboxed before use, and the result of - * `someMethod()` is boxed back. + * the body of `someMethod` is boxed back. The Si and SR are the types + * found in the target `someMethod`; the Ti and TR are the types found in + * the SAM method to be implemented. It is common for `Si` to be different + * from `Ti`. For example, in a Scala function `(x: Int) => x`, + * `S1 = SR = int`, but `T1 = TR = any`, because `scala.Function1` defines + * an `apply` method that erases to using `any`'s. * * Then, we wrap that closure in a class satisfying the expected type. - * For Scala function types, we use the existing - * `scala.scalajs.runtime.AnonFunctionN` from the library. For other - * LMF-capable types, we generate a class on the fly, which looks like - * this: + * For SAM types that do not need any bridges (including all Scala + * function types), we use a `NewLambda` node. + * + * When bridges are required (which is rare), we generate a class on the + * fly. In that case, we "inline" the captures of the typed closure as + * fields of the class, and its body as the body of the main SAM method + * implementation. Overall, it looks like this: * {{{ * class AnonFun extends Object with FunctionalInterface { - * val f: any - * def (f: any) { + * val ...captureI: UI + * def (...captureI: UI) { * super(); - * this.f = f + * ...this.captureI = captureI; + * } + * // main SAM method implementation + * def theSAMMethod(params: Ti...): TR = { + * val ...captureI = this.captureI; + * // inline body of the typed-lambda + * } + * // a bridge + * def theSAMMethod(params: Vi...): VR = { + * this.theSAMMethod(...params.asInstanceOf[Ti]).asInstanceOf[VR] * } - * def theSAMMethod(params: Types...): Type = - * unbox((this.f)(boxParams...)) * } * }}} */ @@ -6293,80 +6413,187 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val Function(paramTrees, Apply( targetTree @ Select(receiver, _), allArgs0)) = originalFunction + // Extract information about the SAM type we are implementing + val samClassSym = originalFunction.tpe.typeSymbolDirect + val (superClass, interfaces, sam, samBridges) = if (isFunctionSymbol(samClassSym)) { + // This is a scala.FunctionN SAM; extend the corresponding AbstractFunctionN class + val arity = paramTrees.size + val superClass = AbstractFunctionClass(arity) + val sam = superClass.info.member(nme.apply) + (superClass, Nil, sam, Nil) + } else { + // This is an arbitrary SAM interface + val samInfo = originalFunction.attachments.get[SAMFunction].getOrElse { + abort(s"Cannot find the SAMFunction attachment on $originalFunction at $pos") + } + (ObjectClass, samClassSym :: Nil, samInfo.sam, samBridgesFor(samInfo)) + } + val captureSyms = - global.delambdafy.FreeVarTraverser.freeVarsOf(originalFunction) + global.delambdafy.FreeVarTraverser.freeVarsOf(originalFunction).toList val target = targetTree.symbol - val params = paramTrees.map(_.symbol) - - val allArgs = allArgs0 map genExpr - - val formalCaptures = captureSyms.toList.map(genParamDef(_, pos)) - val actualCaptures = formalCaptures.map(_.ref) - val formalArgs = params.map(genParamDef(_)) + val isTargetStatic = compileAsStaticMethod(target) - val (allFormalCaptures, body, allActualCaptures) = if (!target.isStaticMember) { - val thisActualCapture = genExpr(receiver) - val thisFormalCapture = js.ParamDef( - freshLocalIdent("this")(receiver.pos), thisOriginalName, - thisActualCapture.tpe, mutable = false)(receiver.pos) - val thisCaptureArg = thisFormalCapture.ref + // Gen actual captures in the local name scope of the enclosing method + val actualCaptures: List[js.Tree] = { + val base = captureSyms.map(genVarRef(_)) + if (isTargetStatic) + base + else + genExpr(receiver) :: base + } - val body = if (isJSType(receiver.tpe) && target.owner != ObjectClass) { - assert(isNonNativeJSClass(target.owner) && !isExposed(target), - s"A Function lambda is trying to call an exposed JS method ${target.fullName}") - genApplyJSClassMethod(thisCaptureArg, target, allArgs) + val closure: js.Closure = withNewLocalNameScope { + // Gen the formal capture params for the closure + val thisFormalCapture: Option[js.ParamDef] = if (isTargetStatic) { + None } else { - genApplyMethodMaybeStatically(thisCaptureArg, target, allArgs) + Some(js.ParamDef( + freshLocalIdent("this")(receiver.pos), thisOriginalName, + toIRType(receiver.tpe), mutable = false)(receiver.pos)) } + val formalCaptures: List[js.ParamDef] = + thisFormalCapture.toList ::: captureSyms.map(genParamDef(_, pos)) - (thisFormalCapture :: formalCaptures, - body, thisActualCapture :: actualCaptures) - } else { - val body = genApplyStatic(target, allArgs) + // Gen the inlined target method body + val genMethodDef = { + genMethodWithCurrentLocalNameScope(consumeDelambdafyTarget(target), + initThisLocalVarName = thisFormalCapture.map(_.name.name)) + } + val js.MethodDef(methodFlags, _, _, methodParams, _, methodBody) = genMethodDef - (formalCaptures, body, actualCaptures) - } + /* If the target method was not supposed to be static, but genMethodDef + * turns out to be static, it means it is a non-exposed method of a JS + * class. The `this` param was turned into a regular param, for which + * we need a `js.VarDef`. + */ + val (maybeThisParamAsVarDef, remainingMethodParams) = { + if (methodFlags.namespace.isStatic && !isTargetStatic) { + val thisParamDef :: remainingMethodParams = methodParams: @unchecked + val thisParamAsVarDef = js.VarDef(thisParamDef.name, thisParamDef.originalName, + thisParamDef.ptpe, thisParamDef.mutable, thisFormalCapture.get.ref) + (thisParamAsVarDef :: Nil, remainingMethodParams) + } else { + (Nil, methodParams) + } + } - val (patchedFormalArgs, paramsLocals) = - patchFunParamsWithBoxes(target, formalArgs, useParamsBeforeLambdaLift = true) + // After that, the args found in the `Function` node had better match the remaining method params + assert(remainingMethodParams.size == allArgs0.size, + s"Arity mismatch: $remainingMethodParams <-> $allArgs0 at $pos") - val patchedBody = - js.Block(paramsLocals :+ ensureResultBoxed(body, target)) + /* Declare each method param as a VarDef, initialized to the corresponding arg. + * In practice, all the args are `This` nodes or `VarRef` nodes, so the + * optimizer will alias those VarDefs away. + * We do this because we have different Symbols, hence different + * encoded LocalIdents. + */ + val methodParamsAsVarDefs = for ((methodParam, arg) <- remainingMethodParams.zip(allArgs0)) yield { + js.VarDef(methodParam.name, methodParam.originalName, methodParam.ptpe, + methodParam.mutable, genExpr(arg)) + } - val closure = js.Closure( - arrow = true, - allFormalCaptures, + val (samParamTypes, samResultType, targetResultType) = enteringPhase(currentRun.posterasurePhase) { + val methodType = sam.tpe.asInstanceOf[MethodType] + (methodType.params.map(_.info), methodType.resultType, target.tpe.finalResultType) + } + + /* Adapt the params and result so that they are boxed from the outside. + * + * TODO In total we generate *3* locals for each original param: the + * patched ParamDef, the VarDef for the unboxed value, and a VarDef for + * the original parameter of the delambdafy target. In theory we only + * need 2: can we make it so? + */ + val formalArgs = paramTrees.map(p => genParamDef(p.symbol)) + val (patchedFormalArgs, paramsLocals) = + patchFunParamsWithBoxes(target, formalArgs, useParamsBeforeLambdaLift = true, fromParamTypes = samParamTypes) + val patchedBodyWithBox = + adaptBoxes(methodBody.get, targetResultType, samResultType) + + // Finally, assemble all the pieces + val fullClosureBody = js.Block( + paramsLocals ::: + maybeThisParamAsVarDef ::: + methodParamsAsVarDefs ::: + patchedBodyWithBox :: + Nil + ) + js.Closure( + js.ClosureFlags.typed, + formalCaptures, patchedFormalArgs, restParam = None, - patchedBody, - allActualCaptures) - - // Wrap the closure in the appropriate box for the SAM type - val funSym = originalFunction.tpe.typeSymbolDirect - if (isFunctionSymbol(funSym)) { - /* This is a scala.FunctionN. We use the existing AnonFunctionN - * wrapper. + resultType = toIRType(underlyingOfEVT(samResultType)), + fullClosureBody, + actualCaptures + ) + } + + // Build the descriptor + val closureType = closure.tpe.asInstanceOf[jstpe.ClosureType] + val descriptor = js.NewLambda.Descriptor( + encodeClassName(superClass), interfaces.map(encodeClassName(_)), + encodeMethodSym(sam).name, closureType.paramTypes, + closureType.resultType) + + /* Wrap the closure in the appropriate box for the SAM type. + * Use a `NewLambda` if we do not need any bridges; otherwise synthesize + * a SAM wrapper class. + */ + if (samBridges.isEmpty) { + // No bridges are needed; we can directly use a NewLambda + js.NewLambda(descriptor, closure)(encodeClassType(samClassSym).toNonNullable) + } else { + /* We need bridges; expand the `NewLambda` into a synthesized class. + * Captures of the closure are turned into fields of the wrapper class. */ - genJSFunctionToScala(closure, params.size) + val formalCaptureTypeRefs = captureSyms.map(sym => toTypeRef(sym.info)) + val allFormalCaptureTypeRefs = + if (isTargetStatic) formalCaptureTypeRefs + else toTypeRef(receiver.tpe) :: formalCaptureTypeRefs + + val ctorName = ir.Names.MethodName.constructor(allFormalCaptureTypeRefs) + val samWrapperClassName = synthesizeSAMWrapper(descriptor, sam, samBridges, closure, ctorName) + js.New(samWrapperClassName, js.MethodIdent(ctorName), closure.captureValues) + } + } + + private def samBridgesFor(samInfo: SAMFunction)(implicit pos: Position): List[Symbol] = { + /* scala/bug#10512: any methods which `samInfo.sam` overrides need + * bridges made for them. + */ + val samBridges = { + import scala.reflect.internal.Flags.BRIDGE + samInfo.synthCls.info.findMembers(excludedFlags = 0L, requiredFlags = BRIDGE).toList + } + + if (samBridges.isEmpty) { + // fast path + Nil } else { - /* This is an arbitrary SAM type (can only happen in 2.12). - * We have to synthesize a class like LambdaMetaFactory would do on - * the JVM. + /* Remove duplicates, e.g., if we override the same method declared + * in two super traits. */ - val sam = originalFunction.attachments.get[SAMFunction].getOrElse { - abort(s"Cannot find the SAMFunction attachment on $originalFunction at $pos") + val builder = List.newBuilder[Symbol] + val seenMethodNames = mutable.Set.empty[MethodName] + + seenMethodNames.add(encodeMethodSym(samInfo.sam).name) + + for (samBridge <- samBridges) { + if (seenMethodNames.add(encodeMethodSym(samBridge).name)) + builder += samBridge } - val samWrapperClassName = synthesizeSAMWrapper(funSym, sam) - js.New(samWrapperClassName, js.MethodIdent(ObjectArgConstructorName), - List(closure)) + builder.result() } } - private def synthesizeSAMWrapper(funSym: Symbol, samInfo: SAMFunction)( + private def synthesizeSAMWrapper(descriptor: js.NewLambda.Descriptor, + sam: Symbol, samBridges: List[Symbol], closure: js.Closure, + ctorName: ir.Names.MethodName)( implicit pos: Position): ClassName = { - val intfName = encodeClassName(funSym) val suffix = { generatedSAMWrapperCount.value += 1 @@ -6377,83 +6604,81 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val thisType = jstpe.ClassType(className, nullable = false) - // val f: Any - val fFieldIdent = js.FieldIdent(FieldName(className, SimpleFieldName("f"))) - val fFieldDef = js.FieldDef(js.MemberFlags.empty, fFieldIdent, - NoOriginalName, jstpe.AnyType) + // val captureI: CaptureTypeI + val captureFieldDefs = for (captureParam <- closure.captureParams) yield { + val simpleFieldName = SimpleFieldName(captureParam.name.name.encoded) + val ident = js.FieldIdent(FieldName(className, simpleFieldName)) + js.FieldDef(js.MemberFlags.empty, ident, captureParam.originalName, captureParam.ptpe) + } - // def this(f: Any) = { this.f = f; super() } + // def this(f: Any) = { ...this.captureI = captureI; super() } val ctorDef = { - val fParamDef = js.ParamDef(js.LocalIdent(LocalName("f")), - NoOriginalName, jstpe.AnyType, mutable = false) + val captureFieldAssignments = for { + (captureFieldDef, captureParam) <- captureFieldDefs.zip(closure.captureParams) + } yield { + js.Assign( + js.Select(js.This()(thisType), captureFieldDef.name)(captureFieldDef.ftpe), + captureParam.ref) + } js.MethodDef( js.MemberFlags.empty.withNamespace(js.MemberNamespace.Constructor), - js.MethodIdent(ObjectArgConstructorName), + js.MethodIdent(ctorName), NoOriginalName, - List(fParamDef), - jstpe.NoType, + closure.captureParams, + jstpe.VoidType, Some(js.Block(List( - js.Assign( - js.Select(js.This()(thisType), fFieldIdent)(jstpe.AnyType), - fParamDef.ref), + js.Block(captureFieldAssignments), js.ApplyStatically(js.ApplyFlags.empty.withConstructor(true), js.This()(thisType), - ir.Names.ObjectClass, - js.MethodIdent(ir.Names.NoArgConstructorName), - Nil)(jstpe.NoType)))))( + jswkn.ObjectClass, + js.MethodIdent(jswkn.NoArgConstructorName), + Nil)(jstpe.VoidType)))))( js.OptimizerHints.empty, Unversioned) } - // Compute the set of method symbols that we need to implement - val sams = { - val samsBuilder = List.newBuilder[Symbol] - val seenMethodNames = mutable.Set.empty[MethodName] - - /* scala/bug#10512: any methods which `samInfo.sam` overrides need - * bridges made for them. - * On Scala < 2.12.5, `synthCls` is polyfilled to `NoSymbol` and hence - * `samBridges` will always be empty. This causes our compiler to be - * bug-compatible on these versions. - */ - val synthCls = samInfo.synthCls - val samBridges = if (synthCls == NoSymbol) { - Nil - } else { - import scala.reflect.internal.Flags.BRIDGE - synthCls.info.findMembers(excludedFlags = 0L, requiredFlags = BRIDGE).toList + /* def samMethod(...closure.params): closure.resultType = { + * val captureI: CaptureTypeI = this.captureI; + * ... + * closure.body + * } + */ + val samMethodDef: js.MethodDef = { + val localCaptureVarDefs = for { + (captureParam, captureFieldDef) <- closure.captureParams.zip(captureFieldDefs) + } yield { + js.VarDef(captureParam.name, captureParam.originalName, captureParam.ptpe, mutable = false, + js.Select(js.This()(thisType), captureFieldDef.name)(captureFieldDef.ftpe)) } - for (sam <- samInfo.sam :: samBridges) { - /* Remove duplicates, e.g., if we override the same method declared - * in two super traits. - */ - if (seenMethodNames.add(encodeMethodSym(sam).name)) - samsBuilder += sam - } + val body = js.Block(localCaptureVarDefs, closure.body) - samsBuilder.result() + js.MethodDef(js.MemberFlags.empty, encodeMethodSym(sam), + originalNameOfMethod(sam), closure.params, closure.resultType, + Some(body))( + js.OptimizerHints.empty, Unversioned) } - // def samMethod(...params): resultType = this.f(...params) - val samMethodDefs = for (sam <- sams) yield { - val jsParams = sam.tpe.params.map(genParamDef(_, pos)) - val resultType = toIRType(sam.tpe.finalResultType) + val adaptBoxesTupled = (adaptBoxes(_, _, _)).tupled + + // def samBridgeMethod(...params): resultType = this.samMethod(...params) // (with adaptBoxes) + val samBridgeMethodDefs = for (samBridge <- samBridges) yield { + val jsParams = samBridge.tpe.params.map(genParamDef(_, pos)) + val resultType = toIRType(samBridge.tpe.finalResultType) val actualParams = enteringPhase(currentRun.posterasurePhase) { - for ((formal, param) <- jsParams.zip(sam.tpe.params)) - yield (formal.ref, param.tpe) - }.map((ensureBoxed _).tupled) + for (((formal, bridgeParam), samParam) <- jsParams.zip(samBridge.tpe.params).zip(sam.tpe.params)) + yield (formal.ref, bridgeParam.tpe, samParam.tpe) + }.map(adaptBoxesTupled) - val call = js.JSFunctionApply( - js.Select(js.This()(thisType), fFieldIdent)(jstpe.AnyType), - actualParams) + val call = js.Apply(js.ApplyFlags.empty, js.This()(thisType), + samMethodDef.name, actualParams)(samMethodDef.resultType) - val body = fromAny(call, enteringPhase(currentRun.posterasurePhase) { - sam.tpe.finalResultType + val body = adaptBoxesTupled(enteringPhase(currentRun.posterasurePhase) { + (call, sam.tpe.finalResultType, samBridge.tpe.finalResultType) }) - js.MethodDef(js.MemberFlags.empty, encodeMethodSym(sam), - originalNameOfMethod(sam), jsParams, resultType, + js.MethodDef(js.MemberFlags.empty, encodeMethodSym(samBridge), + originalNameOfMethod(samBridge), jsParams, resultType, Some(body))( js.OptimizerHints.empty, Unversioned) } @@ -6464,12 +6689,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) NoOriginalName, ClassKind.Class, None, - Some(js.ClassIdent(ir.Names.ObjectClass)), - List(js.ClassIdent(intfName)), + Some(js.ClassIdent(descriptor.superClass)), + descriptor.interfaces.map(js.ClassIdent(_)), None, None, - fields = fFieldDef :: Nil, - methods = ctorDef :: samMethodDefs, + fields = captureFieldDefs, + methods = ctorDef :: samMethodDef :: samBridgeMethodDefs, jsConstructor = None, Nil, Nil, @@ -6482,7 +6707,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } private def patchFunParamsWithBoxes(methodSym: Symbol, - params: List[js.ParamDef], useParamsBeforeLambdaLift: Boolean)( + params: List[js.ParamDef], useParamsBeforeLambdaLift: Boolean, + fromParamTypes: List[Type])( implicit pos: Position): (List[js.ParamDef], List[js.VarDef]) = { // See the comment in genPrimitiveJSArgs for a rationale about this val paramTpes = enteringPhase(currentRun.posterasurePhase) { @@ -6507,26 +6733,33 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } (for { - (param, paramSym) <- params zip paramSyms + ((param, paramSym), fromParamType) <- params.zip(paramSyms).zip(fromParamTypes) } yield { val paramTpe = paramTpes.getOrElse(paramSym.name, paramSym.tpe) - genPatchedParam(param, fromAny(_, paramTpe)) + genPatchedParam(param, adaptBoxes(_, fromParamType, paramTpe), + toIRType(underlyingOfEVT(fromParamType))) }).unzip } - private def genPatchedParam(param: js.ParamDef, rhs: js.VarRef => js.Tree)( + private def genPatchedParam(param: js.ParamDef, rhs: js.VarRef => js.Tree, + fromParamType: jstpe.Type)( implicit pos: Position): (js.ParamDef, js.VarDef) = { val paramNameIdent = param.name val origName = param.originalName val newNameIdent = freshLocalIdent(paramNameIdent.name)(paramNameIdent.pos) val newOrigName = origName.orElse(paramNameIdent.name) - val patchedParam = js.ParamDef(newNameIdent, newOrigName, jstpe.AnyType, + val patchedParam = js.ParamDef(newNameIdent, newOrigName, fromParamType, mutable = false)(param.pos) val paramLocal = js.VarDef(paramNameIdent, origName, param.ptpe, mutable = false, rhs(patchedParam.ref)) (patchedParam, paramLocal) } + private def underlyingOfEVT(tpe: Type): Type = tpe match { + case tpe: ErasedValueType => tpe.erasedUnderlying + case _ => tpe + } + /** Generates a static method instantiating and calling this * DynamicImportThunk's `apply`: * @@ -6587,7 +6820,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Utilities --------------------------------------------------------------- def genVarRef(sym: Symbol)(implicit pos: Position): js.VarRef = - js.VarRef(encodeLocalSym(sym))(toIRType(sym.tpe)) + js.VarRef(encodeLocalSymName(sym))(toIRType(sym.tpe)) def genParamDef(sym: Symbol): js.ParamDef = genParamDef(sym, toIRType(sym.tpe)) @@ -7089,7 +7322,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) def traverse(traverser: ir.Traversers.Traverser): Unit = () - def transform(transformer: ir.Transformers.Transformer, isStat: Boolean)( + def transform(transformer: ir.Transformers.Transformer)( implicit pos: ir.Position): js.Tree = { js.Transient(this) } @@ -7106,38 +7339,7 @@ private object GenJSCode { private val newSimpleMethodName = SimpleMethodName("new") private val ObjectArgConstructorName = - MethodName.constructor(List(jstpe.ClassRef(ir.Names.ObjectClass))) - - private val lengthMethodName = - MethodName("length", Nil, jstpe.IntRef) - private val charAtMethodName = - MethodName("charAt", List(jstpe.IntRef), jstpe.CharRef) - - private val getNameMethodName = - MethodName("getName", Nil, jstpe.ClassRef(ir.Names.BoxedStringClass)) - private val isPrimitiveMethodName = - MethodName("isPrimitive", Nil, jstpe.BooleanRef) - private val isInterfaceMethodName = - MethodName("isInterface", Nil, jstpe.BooleanRef) - private val isArrayMethodName = - MethodName("isArray", Nil, jstpe.BooleanRef) - private val getComponentTypeMethodName = - MethodName("getComponentType", Nil, jstpe.ClassRef(ir.Names.ClassClass)) - private val getSuperclassMethodName = - MethodName("getSuperclass", Nil, jstpe.ClassRef(ir.Names.ClassClass)) - - private val isInstanceMethodName = - MethodName("isInstance", List(jstpe.ClassRef(ir.Names.ObjectClass)), jstpe.BooleanRef) - private val isAssignableFromMethodName = - MethodName("isAssignableFrom", List(jstpe.ClassRef(ir.Names.ClassClass)), jstpe.BooleanRef) - private val castMethodName = - MethodName("cast", List(jstpe.ClassRef(ir.Names.ObjectClass)), jstpe.ClassRef(ir.Names.ObjectClass)) - - private val arrayNewInstanceMethodName = { - MethodName("newInstance", - List(jstpe.ClassRef(ir.Names.ClassClass), jstpe.IntRef), - jstpe.ClassRef(ir.Names.ObjectClass)) - } + MethodName.constructor(List(jswkn.ObjectRef)) private val thisOriginalName = OriginalName("this") @@ -7154,4 +7356,120 @@ private object GenJSCode { case _ => Some((tree, Nil)) } } + + private abstract class JavalibOpBody { + /** Generates the body of this special method, given references to the receiver and parameters. */ + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree + } + + private object JavalibOpBody { + private def checkNotNullIf(arg: js.Tree, checkNulls: Boolean)(implicit pos: ir.Position): js.Tree = + if (checkNulls && arg.tpe.isNullable) js.UnaryOp(js.UnaryOp.CheckNotNull, arg) + else arg + + /* These are case classes for convenience (for the apply method). + * They are not intended for pattern matching. + */ + + /** UnaryOp applying to the `this` parameter. */ + final case class ThisUnaryOp(op: js.UnaryOp.Code) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + assert(args.isEmpty) + js.UnaryOp(op, receiver) + } + } + + /** BinaryOp applying to the `this` parameter and the regular parameter. */ + final case class ThisBinaryOp(op: js.BinaryOp.Code, checkNulls: Boolean = false) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + val List(rhs) = args: @unchecked + js.BinaryOp(op, receiver, checkNotNullIf(rhs, checkNulls)) + } + } + + /** UnaryOp applying to the only regular parameter (`this` is ignored). */ + final case class ArgUnaryOp(op: js.UnaryOp.Code, checkNulls: Boolean = false) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + val List(arg) = args: @unchecked + js.UnaryOp(op, checkNotNullIf(arg, checkNulls)) + } + } + + /** BinaryOp applying to the two regular paramters (`this` is ignored). */ + final case class ArgBinaryOp(op: js.BinaryOp.Code, checkNulls: Boolean = false) extends JavalibOpBody { + def generate(receiver: js.Tree, args: List[js.Tree])(implicit pos: ir.Position): js.Tree = { + val List(lhs, rhs) = args: @unchecked + js.BinaryOp(op, checkNotNullIf(lhs, checkNulls), checkNotNullIf(rhs, checkNulls)) + } + } + } + + /** Methods of the javalib whose body must be replaced by a dedicated + * UnaryOp or BinaryOp. + * + * We use IR encoded names to identify them, rather than scalac Symbols. + * There is no fundamental reason for that. It makes it easier to define + * this map in a declarative way, especially when overloaded methods are + * concerned (Array.newInstance). It also allows to define it independently + * of the Global instance, but that is marginal. + */ + private lazy val JavalibMethodsWithOpBody: Map[(ClassName, MethodName), JavalibOpBody] = { + import JavalibOpBody._ + import js.{UnaryOp => unop, BinaryOp => binop} + import jstpe.{BooleanRef => Z, CharRef => C, IntRef => I, LongRef => J, FloatRef => F, DoubleRef => D} + import MethodName.{apply => m} + + val O = jswkn.ObjectRef + val CC = jstpe.ClassRef(jswkn.ClassClass) + val T = jstpe.ClassRef(jswkn.BoxedStringClass) + + val byClass: Map[ClassName, Map[MethodName, JavalibOpBody]] = Map( + jswkn.BoxedIntegerClass.withSuffix("$") -> Map( + m("divideUnsigned", List(I, I), I) -> ArgBinaryOp(binop.Int_unsigned_/), + m("remainderUnsigned", List(I, I), I) -> ArgBinaryOp(binop.Int_unsigned_%) + ), + jswkn.BoxedLongClass.withSuffix("$") -> Map( + m("divideUnsigned", List(J, J), J) -> ArgBinaryOp(binop.Long_unsigned_/), + m("remainderUnsigned", List(J, J), J) -> ArgBinaryOp(binop.Long_unsigned_%) + ), + jswkn.BoxedFloatClass.withSuffix("$") -> Map( + m("floatToIntBits", List(F), I) -> ArgUnaryOp(unop.Float_toBits), + m("intBitsToFloat", List(I), F) -> ArgUnaryOp(unop.Float_fromBits) + ), + jswkn.BoxedDoubleClass.withSuffix("$") -> Map( + m("doubleToLongBits", List(D), J) -> ArgUnaryOp(unop.Double_toBits), + m("longBitsToDouble", List(J), D) -> ArgUnaryOp(unop.Double_fromBits) + ), + jswkn.BoxedStringClass -> Map( + m("length", Nil, I) -> ThisUnaryOp(unop.String_length), + m("charAt", List(I), C) -> ThisBinaryOp(binop.String_charAt) + ), + jswkn.ClassClass -> Map( + // Unary operators + m("getName", Nil, T) -> ThisUnaryOp(unop.Class_name), + m("isPrimitive", Nil, Z) -> ThisUnaryOp(unop.Class_isPrimitive), + m("isInterface", Nil, Z) -> ThisUnaryOp(unop.Class_isInterface), + m("isArray", Nil, Z) -> ThisUnaryOp(unop.Class_isArray), + m("getComponentType", Nil, CC) -> ThisUnaryOp(unop.Class_componentType), + m("getSuperclass", Nil, CC) -> ThisUnaryOp(unop.Class_superClass), + // Binary operators + m("isInstance", List(O), Z) -> ThisBinaryOp(binop.Class_isInstance), + m("isAssignableFrom", List(CC), Z) -> ThisBinaryOp(binop.Class_isAssignableFrom, checkNulls = true), + m("cast", List(O), O) -> ThisBinaryOp(binop.Class_cast) + ), + ClassName("java.lang.System$") -> Map( + m("identityHashCode", List(O), I) -> ArgUnaryOp(unop.IdentityHashCode) + ), + ClassName("java.lang.reflect.Array$") -> Map( + m("newInstance", List(CC, I), O) -> ArgBinaryOp(binop.Class_newArray, checkNulls = true) + ) + ) + + for { + (cls, methods) <- byClass + (methodName, body) <- methods + } yield { + (cls, methodName) -> body + } + } } diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala index beca736586..bcac2098ea 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala @@ -20,7 +20,7 @@ import scala.reflect.{ClassTag, classTag} import scala.reflect.internal.Flags import org.scalajs.ir -import org.scalajs.ir.{Trees => js, Types => jstpe} +import org.scalajs.ir.{Trees => js, Types => jstpe, WellKnownNames => jswkn} import org.scalajs.ir.Names.LocalName import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.Trees.OptimizerHints @@ -684,7 +684,7 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { val superClass = { val superClassSym = currentClassSym.superClass if (isNestedJSClass(superClassSym)) { - js.VarRef(js.LocalIdent(JSSuperClassParamName))(jstpe.AnyType) + js.VarRef(JSSuperClassParamName)(jstpe.AnyType) } else { js.LoadJSConstructor(encodeClassName(superClassSym)) } @@ -870,7 +870,7 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { } // #4684 If the getter returns void, we must "box" it by returning undefined - if (callGetter.tpe == jstpe.NoType) + if (callGetter.tpe == jstpe.VoidType) js.Block(callGetter, js.Undefined()) else callGetter @@ -970,12 +970,10 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { InstanceOfTypeTest(tpe.valueClazz.typeConstructor) case _ => - import org.scalajs.ir.Names - (toIRType(tpe): @unchecked) match { case jstpe.AnyType | jstpe.AnyNotNullType => NoTypeTest - case jstpe.NoType => PrimitiveTypeTest(jstpe.UndefType, 0) + case jstpe.VoidType => PrimitiveTypeTest(jstpe.UndefType, 0) case jstpe.BooleanType => PrimitiveTypeTest(jstpe.BooleanType, 1) case jstpe.CharType => PrimitiveTypeTest(jstpe.CharType, 2) case jstpe.ByteType => PrimitiveTypeTest(jstpe.ByteType, 3) @@ -985,8 +983,8 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { case jstpe.FloatType => PrimitiveTypeTest(jstpe.FloatType, 7) case jstpe.DoubleType => PrimitiveTypeTest(jstpe.DoubleType, 8) - case jstpe.ClassType(Names.BoxedUnitClass, _) => PrimitiveTypeTest(jstpe.UndefType, 0) - case jstpe.ClassType(Names.BoxedStringClass, _) => PrimitiveTypeTest(jstpe.StringType, 9) + case jstpe.ClassType(jswkn.BoxedUnitClass, _) => PrimitiveTypeTest(jstpe.UndefType, 0) + case jstpe.ClassType(jswkn.BoxedStringClass, _) => PrimitiveTypeTest(jstpe.StringType, 9) case jstpe.ClassType(_, _) => InstanceOfTypeTest(tpe) case jstpe.ArrayType(_, _) => InstanceOfTypeTest(tpe) @@ -1022,7 +1020,7 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { private def genThrowTypeError(msg: String = "No matching overload")( implicit pos: Position): js.Tree = { - js.Throw(js.StringLiteral(msg)) + js.UnaryOp(js.UnaryOp.Throw, js.StringLiteral(msg)) } class FormalArgsRegistry(minArgc: Int, needsRestParam: Boolean) { @@ -1053,7 +1051,7 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { def genArgRef(index: Int)(implicit pos: Position): js.Tree = { if (index < minArgc) - js.VarRef(js.LocalIdent(fixedParamNames(index)))(jstpe.AnyType) + js.VarRef(fixedParamNames(index))(jstpe.AnyType) else js.JSSelect(genRestArgRef(), js.IntLiteral(index - minArgc)) } @@ -1073,16 +1071,16 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { def genRestArgRef()(implicit pos: Position): js.Tree = { assert(needsRestParam, s"trying to generate a reference to non-existent rest param at $pos") - js.VarRef(js.LocalIdent(restParamName))(jstpe.AnyType) + js.VarRef(restParamName)(jstpe.AnyType) } def genAllArgsRefsForForwarder()(implicit pos: Position): List[js.TreeOrJSSpread] = { val fixedArgRefs = fixedParamNames.toList.map { paramName => - js.VarRef(js.LocalIdent(paramName))(jstpe.AnyType) + js.VarRef(paramName)(jstpe.AnyType) } if (needsRestParam) { - val restArgRef = js.VarRef(js.LocalIdent(restParamName))(jstpe.AnyType) + val restArgRef = js.VarRef(restParamName)(jstpe.AnyType) fixedArgRefs :+ js.JSSpread(restArgRef) } else { fixedArgRefs diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala index 8214985b6a..e91b74d4ff 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala @@ -47,6 +47,8 @@ trait JSDefinitions { lazy val JSPackage_native = getMemberMethod(ScalaJSJSPackageModule, newTermName("native")) lazy val JSPackage_undefined = getMemberMethod(ScalaJSJSPackageModule, newTermName("undefined")) lazy val JSPackage_dynamicImport = getMemberMethod(ScalaJSJSPackageModule, newTermName("dynamicImport")) + lazy val JSPackage_async = getMemberMethod(ScalaJSJSPackageModule, newTermName("async")) + lazy val JSPackage_await = getMemberMethod(ScalaJSJSPackageModule, newTermName("await")) lazy val JSNativeAnnotation = getRequiredClass("scala.scalajs.js.native") @@ -114,6 +116,11 @@ trait JSDefinitions { lazy val Special_unwrapFromThrowable = getMemberMethod(SpecialPackageModule, newTermName("unwrapFromThrowable")) lazy val Special_debugger = getMemberMethod(SpecialPackageModule, newTermName("debugger")) + lazy val WasmJSPIModule = getRequiredModule("scala.scalajs.js.wasm.JSPI") + lazy val WasmJSPIModuleClass = WasmJSPIModule.moduleClass + lazy val WasmJSPI_allowOrphanJSAwaitModule = getMemberModule(WasmJSPIModuleClass, newTermName("allowOrphanJSAwait")) + lazy val WasmJSPI_allowOrphanJSAwaitModuleClass = WasmJSPI_allowOrphanJSAwaitModule.moduleClass + lazy val RuntimePackageModule = getPackageObject("scala.scalajs.runtime") lazy val Runtime_toScalaVarArgs = getMemberMethod(RuntimePackageModule, newTermName("toScalaVarArgs")) lazy val Runtime_toJSVarArgs = getMemberMethod(RuntimePackageModule, newTermName("toJSVarArgs")) @@ -124,9 +131,16 @@ trait JSDefinitions { lazy val Runtime_withContextualJSClassValue = getMemberMethod(RuntimePackageModule, newTermName("withContextualJSClassValue")) lazy val Runtime_privateFieldsSymbol = getMemberMethod(RuntimePackageModule, newTermName("privateFieldsSymbol")) lazy val Runtime_linkingInfo = getMemberMethod(RuntimePackageModule, newTermName("linkingInfo")) - lazy val Runtime_identityHashCode = getMemberMethod(RuntimePackageModule, newTermName("identityHashCode")) lazy val Runtime_dynamicImport = getMemberMethod(RuntimePackageModule, newTermName("dynamicImport")) + lazy val LinkingInfoModule = getRequiredModule("scala.scalajs.LinkingInfo") + lazy val LinkingInfo_linkTimeIf = getMemberMethod(LinkingInfoModule, newTermName("linkTimeIf")) + lazy val LinkingInfo_linkTimePropertyBoolean = getMemberMethod(LinkingInfoModule, newTermName("linkTimePropertyBoolean")) + lazy val LinkingInfo_linkTimePropertyInt = getMemberMethod(LinkingInfoModule, newTermName("linkTimePropertyInt")) + lazy val LinkingInfo_linkTimePropertyString = getMemberMethod(LinkingInfoModule, newTermName("linkTimePropertyString")) + + lazy val LinkTimePropertyAnnotation = getRequiredClass("scala.scalajs.annotation.linkTimeProperty") + lazy val DynamicImportThunkClass = getRequiredClass("scala.scalajs.runtime.DynamicImportThunk") lazy val DynamicImportThunkClass_apply = getMemberMethod(DynamicImportThunkClass, nme.apply) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala index 51ec7de709..263f1def30 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala @@ -17,7 +17,7 @@ import scala.collection.mutable import scala.tools.nsc._ import org.scalajs.ir -import org.scalajs.ir.{Trees => js, Types => jstpe} +import org.scalajs.ir.{Trees => js, Types => jstpe, WellKnownNames => jswkn} import org.scalajs.ir.Names.{LocalName, LabelName, SimpleFieldName, FieldName, SimpleMethodName, MethodName, ClassName} import org.scalajs.ir.OriginalName import org.scalajs.ir.OriginalName.NoOriginalName @@ -91,7 +91,7 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent { case None => inner case Some(labelName) => - js.Labeled(js.LabelIdent(labelName), tpe, inner) + js.Labeled(labelName, tpe, inner) } } } @@ -151,29 +151,26 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent { private def freshLabelName(base: LabelName): LabelName = freshNameGeneric(base, usedLabelNames)(_.withSuffix(_)) - private def freshLabelName(base: String): LabelName = + def freshLabelName(base: String): LabelName = freshLabelName(LabelName(base)) - def freshLabelIdent(base: String)(implicit pos: ir.Position): js.LabelIdent = - js.LabelIdent(freshLabelName(base)) - private def labelSymbolName(sym: Symbol): LabelName = labelSymbolNames.getOrElseUpdate(sym, freshLabelName(sym.name.toString)) - def getEnclosingReturnLabel()(implicit pos: ir.Position): js.LabelIdent = { + def getEnclosingReturnLabel()(implicit pos: Position): LabelName = { val box = returnLabelName.get if (box == null) throw new IllegalStateException(s"No enclosing returnable scope at $pos") if (box.value.isEmpty) box.value = Some(freshLabelName("_return")) - js.LabelIdent(box.value.get) + box.value.get } // Encoding methods ---------------------------------------------------------- - def encodeLabelSym(sym: Symbol)(implicit pos: Position): js.LabelIdent = { + def encodeLabelSym(sym: Symbol): LabelName = { require(sym.isLabel, "encodeLabelSym called with non-label symbol: " + sym) - js.LabelIdent(labelSymbolName(sym)) + labelSymbolName(sym) } def encodeFieldSym(sym: Symbol)(implicit pos: Position): js.FieldIdent = { @@ -240,7 +237,7 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent { def encodeDynamicImportForwarderIdent(params: List[Symbol])( implicit pos: Position): js.MethodIdent = { val paramTypeRefs = params.map(sym => paramOrResultTypeRef(sym.tpe)) - val resultTypeRef = jstpe.ClassRef(ir.Names.ObjectClass) + val resultTypeRef = jstpe.ClassRef(jswkn.ObjectClass) val methodName = MethodName(dynamicImportForwarderSimpleName, paramTypeRefs, resultTypeRef) @@ -256,7 +253,10 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent { } } - def encodeLocalSym(sym: Symbol)(implicit pos: Position): js.LocalIdent = { + def encodeLocalSym(sym: Symbol)(implicit pos: Position): js.LocalIdent = + js.LocalIdent(encodeLocalSymName(sym)) + + def encodeLocalSymName(sym: Symbol): LocalName = { /* The isValueParameter case is necessary to work around an internal bug * of scalac: for some @varargs methods, the owner of some parameters is * the enclosing class rather the method, so !sym.owner.isClass fails. @@ -266,7 +266,7 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent { require(sym.isValueParameter || (!sym.owner.isClass && sym.isTerm && !sym.isMethod && !sym.isModule), "encodeLocalSym called with non-local symbol: " + sym) - js.LocalIdent(localSymbolName(sym)) + localSymbolName(sym) } def encodeClassType(sym: Symbol): jstpe.Type = { @@ -288,13 +288,13 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent { assert(!sym.isPrimitiveValueClass, s"Illegal encodeClassName(${sym.fullName}") if (sym == jsDefinitions.HackedStringClass) { - ir.Names.BoxedStringClass + jswkn.BoxedStringClass } else if (sym == jsDefinitions.HackedStringModClass) { BoxedStringModuleClassName } else if (sym == definitions.BoxedUnitClass || sym == jsDefinitions.BoxedUnitModClass) { // Rewire scala.runtime.BoxedUnit to java.lang.Void, as the IR expects // BoxedUnit$ is a JVM artifact - ir.Names.BoxedUnitClass + jswkn.BoxedUnitClass } else { ClassName(sym.fullName + (if (needsModuleClassSuffix(sym)) "$" else "")) } diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala index df5ff293db..a199b87f98 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala @@ -51,13 +51,14 @@ abstract class JSPrimitives { final val JS_IMPORT = JS_NEW_TARGET + 1 // js.import.apply(specifier) final val JS_IMPORT_META = JS_IMPORT + 1 // js.import.meta - final val CONSTRUCTOROF = JS_IMPORT_META + 1 // runtime.constructorOf(clazz) + 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 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 - final val LINKING_INFO = WITH_CONTEXTUAL_JS_CLASS_VALUE + 1 // runtime.linkingInfo - final val IDENTITY_HASH_CODE = LINKING_INFO + 1 // runtime.identityHashCode - final val DYNAMIC_IMPORT = IDENTITY_HASH_CODE + 1 // runtime.dynamicImport + final val DYNAMIC_IMPORT = WITH_CONTEXTUAL_JS_CLASS_VALUE + 1 // runtime.dynamicImport final val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals final val IN = STRICT_EQ + 1 // js.special.in @@ -69,8 +70,10 @@ abstract class JSPrimitives { final val WRAP_AS_THROWABLE = JS_TRY_CATCH + 1 // js.special.wrapAsThrowable final val UNWRAP_FROM_THROWABLE = WRAP_AS_THROWABLE + 1 // js.special.unwrapFromThrowable final val DEBUGGER = UNWRAP_FROM_THROWABLE + 1 // js.special.debugger + final val LINKTIME_IF = DEBUGGER + 1 // LinkingInfo.linkTimeIf + final val LINKTIME_PROPERTY = LINKTIME_IF + 1 // LinkingInfo.linkTimePropertyXXX - final val LastJSPrimitiveCode = DEBUGGER + final val LastJSPrimitiveCode = LINKTIME_PROPERTY /** Initialize the map of primitive methods (for GenJSCode) */ def init(): Unit = initWithPrimitives(addPrimitive) @@ -96,6 +99,8 @@ abstract class JSPrimitives { addPrimitive(JSPackage_typeOf, TYPEOF) addPrimitive(JSPackage_native, JS_NATIVE) + addPrimitive(JSPackage_async, JS_ASYNC) + addPrimitive(JSPackage_await, JS_AWAIT) addPrimitive(BoxedUnit_UNIT, UNITVAL) @@ -109,8 +114,6 @@ abstract class JSPrimitives { addPrimitive(Runtime_createLocalJSClass, CREATE_LOCAL_JS_CLASS) addPrimitive(Runtime_withContextualJSClassValue, WITH_CONTEXTUAL_JS_CLASS_VALUE) - addPrimitive(Runtime_linkingInfo, LINKING_INFO) - addPrimitive(Runtime_identityHashCode, IDENTITY_HASH_CODE) addPrimitive(Runtime_dynamicImport, DYNAMIC_IMPORT) addPrimitive(Special_strictEquals, STRICT_EQ) @@ -123,6 +126,11 @@ abstract class JSPrimitives { addPrimitive(Special_wrapAsThrowable, WRAP_AS_THROWABLE) addPrimitive(Special_unwrapFromThrowable, UNWRAP_FROM_THROWABLE) addPrimitive(Special_debugger, DEBUGGER) + + addPrimitive(LinkingInfo_linkTimeIf, LINKTIME_IF) + addPrimitive(LinkingInfo_linkTimePropertyBoolean, LINKTIME_PROPERTY) + addPrimitive(LinkingInfo_linkTimePropertyInt, LINKTIME_PROPERTY) + addPrimitive(LinkingInfo_linkTimePropertyString, LINKTIME_PROPERTY) } def isJavaScriptPrimitive(code: Int): Boolean = diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSExports.scala b/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSExports.scala index 9c2b0b5c62..e9217c04a7 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSExports.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSExports.scala @@ -16,8 +16,8 @@ import scala.collection.mutable import scala.tools.nsc.Global -import org.scalajs.ir.Names.DefaultModuleID import org.scalajs.ir.Trees.TopLevelExportDef.isValidTopLevelExportName +import org.scalajs.ir.WellKnownNames.DefaultModuleID /** * Prepare export generation diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSInterop.scala b/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSInterop.scala index 2a59a931bc..592b9aa381 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSInterop.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/PrepJSInterop.scala @@ -475,7 +475,7 @@ abstract class PrepJSInterop[G <: Global with Singleton](val global: G) */ val superCtorCall = gen.mkMethodCall( Super(clsSym, tpnme.EMPTY), - ObjectClass.primaryConstructor, Nil, Nil) + DynamicImportThunkClass.primaryConstructor, Nil, Nil) // class $anon extends DynamicImportThunk val clsDef = ClassDef(clsSym, List( diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/TypeConversions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/TypeConversions.scala index 97db07f24f..eae31fdb14 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/TypeConversions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/TypeConversions.scala @@ -25,7 +25,7 @@ trait TypeConversions[G <: Global with Singleton] extends SubComponent { private lazy val primitiveIRTypeMap: Map[Symbol, Types.Type] = { Map( - UnitClass -> Types.NoType, + UnitClass -> Types.VoidType, BooleanClass -> Types.BooleanType, CharClass -> Types.CharType, ByteClass -> Types.ByteType, diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/DiverseErrorsTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/DiverseErrorsTest.scala index da1360b581..9ed869cbcc 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/DiverseErrorsTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/DiverseErrorsTest.scala @@ -26,13 +26,8 @@ class DiverseErrorsTest extends DirectTest with TestHelpers { private def version = scala.util.Properties.versionNumberString - private val allowsSingletonClassOf = ( - !version.startsWith("2.12.") && - version != "2.13.0" && - version != "2.13.1" && - version != "2.13.2" && - version != "2.13.3" - ) + private val allowsSingletonClassOf = + !version.startsWith("2.12.") && version != "2.13.3" @Test def noIsInstanceOnJS(): Unit = { diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSAsyncAwaitTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSAsyncAwaitTest.scala new file mode 100644 index 0000000000..d8147fad0a --- /dev/null +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSAsyncAwaitTest.scala @@ -0,0 +1,83 @@ +/* + * 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 org.scalajs.nscplugin.test + +import org.scalajs.nscplugin.test.util._ +import org.junit.Test + +// scalastyle:off line.size.limit + +class JSAsyncAwaitTest extends DirectTest with TestHelpers { + + override def preamble: String = + """import scala.scalajs.js + """ + + @Test + def orphanAwait(): Unit = { + """ + class A { + def foo(x: js.Promise[Int]): Int = + js.await(x) + } + """ hasErrors + """ + |newSource1.scala:5: error: Illegal use of js.await(). + |It can only be used inside a js.async {...} block, without any lambda, + |by-name argument or nested method in-between. + |If you compile for WebAssembly, you can allow arbitrary js.await() + |calls by adding the following import: + |import scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait + | js.await(x) + | ^ + """ + + """ + class A { + def foo(x: js.Promise[Int]): js.Promise[Int] = js.async { + val f: () => Int = () => js.await(x) + f() + } + } + """ hasErrors + """ + |newSource1.scala:5: error: Illegal use of js.await(). + |It can only be used inside a js.async {...} block, without any lambda, + |by-name argument or nested method in-between. + |If you compile for WebAssembly, you can allow arbitrary js.await() + |calls by adding the following import: + |import scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait + | val f: () => Int = () => js.await(x) + | ^ + """ + + """ + class A { + def foo(x: js.Promise[Int]): js.Promise[Int] = js.async { + def f(): Int = js.await(x) + f() + } + } + """ hasErrors + """ + |newSource1.scala:5: error: Illegal use of js.await(). + |It can only be used inside a js.async {...} block, without any lambda, + |by-name argument or nested method in-between. + |If you compile for WebAssembly, you can allow arbitrary js.await() + |calls by adding the following import: + |import scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait + | def f(): Int = js.await(x) + | ^ + """ + } +} diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSGlobalScopeTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSGlobalScopeTest.scala index 6d5628abf2..19d3f0f8d3 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSGlobalScopeTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSGlobalScopeTest.scala @@ -342,7 +342,7 @@ class JSGlobalScopeTest extends DirectTest with TestHelpers { "extends", "false", "finally", "for", "function", "if", "implements", "import", "in", "instanceof", "interface", "let", "new", "null", "package", "private", "protected", "public", "return", "static", - "super", "switch", "this", "throw", "true", "try", "typeof", "var", + "super", "switch", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield") for (reservedIdentifier <- reservedIdentifiers) { @@ -497,4 +497,34 @@ class JSGlobalScopeTest extends DirectTest with TestHelpers { } } + @Test + def rejectAssignmentToGlobalThis(): Unit = { + """ + import scala.scalajs.js + import scala.scalajs.js.annotation._ + + object Main { + def main(): Unit = { + js.Dynamic.global.`this` = 0 + GlobalScope.globalThis = 0 + } + } + + @js.native + @JSGlobalScope + object GlobalScope extends js.Any { + @JSName("this") + var globalThis: Any = js.native + } + """ hasErrors + s""" + |newSource1.scala:44: error: Illegal assignment to global this. + | js.Dynamic.global.`this` = 0 + | ^ + |newSource1.scala:45: error: Illegal assignment to global this. + | GlobalScope.globalThis = 0 + | ^ + """ + } + } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSInteropTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSInteropTest.scala index 582aba2f40..8d79f251a3 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSInteropTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSInteropTest.scala @@ -4419,17 +4419,11 @@ class JSInteropTest extends DirectTest with TestHelpers { val postUnarySpace = { val hasNoSpace = { - version == "2.12.2" || - version == "2.12.3" || - version == "2.12.4" || - version == "2.12.5" || version == "2.12.6" || version == "2.12.7" || version == "2.12.8" || version == "2.12.9" || - version == "2.12.10" || - version == "2.13.0" || - version == "2.13.1" + version == "2.12.10" } if (hasNoSpace) "" else " " diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSNewTargetTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSNewTargetTest.scala index edfa1afd01..2e2c1664f2 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSNewTargetTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSNewTargetTest.scala @@ -122,16 +122,6 @@ class JSNewTargetTest extends DirectTest with TestHelpers { } """ hasErrors """ - |newSource1.scala:4: error: Illegal use of js.`new`.target. - |It can only be used in the constructor of a JS class, as a statement or in the rhs of a val or var. - |It cannot be used inside a lambda or by-name parameter, nor in any other location. - | val x = () => js.`new`.target - | ^ - |newSource1.scala:5: error: Illegal use of js.`new`.target. - |It can only be used in the constructor of a JS class, as a statement or in the rhs of a val or var. - |It cannot be used inside a lambda or by-name parameter, nor in any other location. - | val y = Option(null).getOrElse(js.`new`.target) - | ^ |newSource1.scala:6: error: Illegal use of js.`new`.target. |It can only be used in the constructor of a JS class, as a statement or in the rhs of a val or var. |It cannot be used inside a lambda or by-name parameter, nor in any other location. @@ -142,6 +132,16 @@ class JSNewTargetTest extends DirectTest with TestHelpers { |It cannot be used inside a lambda or by-name parameter, nor in any other location. | val w: js.ThisFunction0[Any, Any] = (x: Any) => js.`new`.target | ^ + |newSource1.scala:4: error: Illegal use of js.`new`.target. + |It can only be used in the constructor of a JS class, as a statement or in the rhs of a val or var. + |It cannot be used inside a lambda or by-name parameter, nor in any other location. + | val x = () => js.`new`.target + | ^ + |newSource1.scala:5: error: Illegal use of js.`new`.target. + |It can only be used in the constructor of a JS class, as a statement or in the rhs of a val or var. + |It cannot be used inside a lambda or by-name parameter, nor in any other location. + | val y = Option(null).getOrElse(js.`new`.target) + | ^ """ } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala index 3513fff2e3..4eedcb3447 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSSAMTest.scala @@ -126,10 +126,10 @@ class JSSAMTest extends DirectTest with TestHelpers { } """ hasErrors """ - |newSource1.scala:14: error: The SAM or apply method for a js.ThisFunction must have a leading non-varargs parameter + |newSource1.scala:14: error: The apply method for a js.ThisFunction must have a leading non-varargs parameter | val badThisFunction1: BadThisFunction1 = () => 42 | ^ - |newSource1.scala:15: error: The SAM or apply method for a js.ThisFunction must have a leading non-varargs parameter + |newSource1.scala:15: error: The apply method for a js.ThisFunction must have a leading non-varargs parameter | val badThisFunction2: BadThisFunction2 = args => args.size | ^ """ diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/LinkTimeIfTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/LinkTimeIfTest.scala new file mode 100644 index 0000000000..881c0e9a2f --- /dev/null +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/LinkTimeIfTest.scala @@ -0,0 +1,109 @@ +/* + * 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 org.scalajs.nscplugin.test + +import util._ + +import org.junit.Test +import org.junit.Assert._ + +// scalastyle:off line.size.limit + +class LinkTimeIfTest extends TestHelpers { + override def preamble: String = "import scala.scalajs.LinkingInfo._" + + private final val IllegalLinkTimeIfArgMessage = { + "Illegal expression in the condition of a linkTimeIf. " + + "Valid expressions are: boolean and int primitives; " + + "references to link-time properties; " + + "primitive operations on booleans; " + + "and comparisons on ints." + } + + @Test + def linkTimeErrorInvalidOp(): Unit = { + """ + object A { + def foo = + linkTimeIf((esVersion + 1) < ESVersion.ES2015) { } { } + } + """ hasErrors + s""" + |newSource1.scala:4: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf((esVersion + 1) < ESVersion.ES2015) { } { } + | ^ + """ + } + + @Test + def linkTimeErrorInvalidEntities(): Unit = { + """ + object A { + def foo(x: String) = { + val bar = 1 + linkTimeIf(bar == 0) { } { } + } + } + """ hasErrors + s""" + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(bar == 0) { } { } + | ^ + """ + + // String comparison is a `BinaryOp.===`, which is not allowed + """ + object A { + def foo(x: String) = + linkTimeIf("foo" == x) { } { } + } + """ hasErrors + s""" + |newSource1.scala:4: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf("foo" == x) { } { } + | ^ + """ + + """ + object A { + def bar = true + def foo(x: String) = + linkTimeIf(bar || !bar) { } { } + } + """ hasErrors + s""" + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(bar || !bar) { } { } + | ^ + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(bar || !bar) { } { } + | ^ + """ + } + + @Test + def linkTimeCondInvalidTree(): Unit = { + """ + object A { + def bar = true + def foo(x: String) = + linkTimeIf(if (bar) true else false) { } { } + } + """ hasErrors + s""" + |newSource1.scala:5: error: $IllegalLinkTimeIfArgMessage + | linkTimeIf(if (bar) true else false) { } { } + | ^ + """ + } +} diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala index 7e140ebd38..b10bef4b95 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala @@ -170,14 +170,14 @@ class OptimizationTest extends JSASTTest { // Verify the optimized emitted code for 'new js.Object' and 'new js.Array' """ import scala.scalajs.js - class A { val o = new js.Object val a = new js.Array } """. - hasNot("any reference to the global scope") { - case js.JSLinkingInfo() => + hasNot("any reference to the global scope nor loading JS constructor") { + case js.JSGlobalRef(_) => + case js.LoadJSConstructor(_) => } } @@ -250,7 +250,7 @@ class OptimizationTest extends JSASTTest { } } """.hasNot("non-return labeled block") { - case js.Labeled(name, _, _) if !name.name.nameString.startsWith("_return") => + case js.Labeled(name, _, _) if !name.nameString.startsWith("_return") => } } @@ -323,7 +323,7 @@ class OptimizationTest extends JSASTTest { } } """.hasNot("non-return labeled block") { - case js.Labeled(name, _, _) if !name.name.nameString.startsWith("_return") => + case js.Labeled(name, _, _) if !name.nameString.startsWith("_return") => } } @@ -519,7 +519,7 @@ class OptimizationTest extends JSASTTest { } } """.hasNot("WrapAsThrowable") { - case js.WrapAsThrowable(_) => + case js.UnaryOp(js.UnaryOp.WrapAsThrowable, _) => } // Confidence check @@ -536,7 +536,7 @@ class OptimizationTest extends JSASTTest { } } """.hasExactly(1, "WrapAsThrowable") { - case js.WrapAsThrowable(_) => + case js.UnaryOp(js.UnaryOp.WrapAsThrowable, _) => } } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/StaticForwardersWarningsTopLevelOnlyTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/StaticForwardersWarningsTopLevelOnlyTest.scala index e0781dcf1b..1b2438655e 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/StaticForwardersWarningsTopLevelOnlyTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/StaticForwardersWarningsTopLevelOnlyTest.scala @@ -24,22 +24,6 @@ class StaticForwardersWarningsTopLevelOnlyTest extends DirectTest with TestHelpe @Test def warnWhenAvoidingStaticForwardersForTopLevelObject: Unit = { - val jvmBackendIssuesWarningOfItsOwn = { - scalaVersion != "2.12.2" && - scalaVersion != "2.12.3" && - scalaVersion != "2.12.4" - } - val jvmBackendMessage = if (!jvmBackendIssuesWarningOfItsOwn) { - "" - } else { - """ - |newSource1.scala:4: warning: Generated class a differs only in case from A. - | Such classes will overwrite one another on case-insensitive filesystems. - | object a { - | ^ - """ - } - """ class A @@ -50,7 +34,11 @@ class StaticForwardersWarningsTopLevelOnlyTest extends DirectTest with TestHelpe s""" |newSource1.scala:4: warning: Not generating the static forwarders of a because its name differs only in case from the name of another class or trait in this compilation unit. | object a { - | ^$jvmBackendMessage + | ^ + |newSource1.scala:4: warning: Generated class a differs only in case from A. + | Such classes will overwrite one another on case-insensitive filesystems. + | object a { + | ^ """ } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/util/VersionDependentUtils.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/util/VersionDependentUtils.scala index 5b54f73a1b..7e52a1edf8 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/util/VersionDependentUtils.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/util/VersionDependentUtils.scala @@ -15,23 +15,12 @@ package org.scalajs.nscplugin.test.util object VersionDependentUtils { val scalaVersion = scala.util.Properties.versionNumberString - /** Does the current Scala version support the `@nowarn` annotation? */ - val scalaSupportsNoWarn = { - !scalaVersion.startsWith("2.12.") && - scalaVersion != "2.13.0" && - scalaVersion != "2.13.1" - } + private val isScala212 = scalaVersion.startsWith("2.12.") - private val usesColonInMethodSig = { - /* Yes, this is the same test as in scalaSupportsNoWarn, but that's - * completely coincidental, so we have a copy. - */ - !scalaVersion.startsWith("2.12.") && - scalaVersion != "2.13.0" && - scalaVersion != "2.13.1" - } + /** Does the current Scala version support the `@nowarn` annotation? */ + val scalaSupportsNoWarn = !isScala212 def methodSig(params: String, resultType: String): String = - if (usesColonInMethodSig) params + ": " + resultType + if (!isScala212) params + ": " + resultType else params + resultType } diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala index d37bf739e7..599e9e8c1c 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -136,9 +136,9 @@ object Hashers { } private final class TreeHasher { - private[this] val digestBuilder = new SHA1.DigestBuilder + private val digestBuilder = new SHA1.DigestBuilder - private[this] val digestStream = { + private val digestStream = { new DataOutputStream(new OutputStream { def write(b: Int): Unit = digestBuilder.update(b.toByte) @@ -185,7 +185,7 @@ object Hashers { case Labeled(label, tpe, body) => mixTag(TagLabeled) - mixLabelIdent(label) + mixName(label) mixType(tpe) mixTree(body) @@ -197,7 +197,7 @@ object Hashers { case Return(expr, label) => mixTag(TagReturn) mixTree(expr) - mixLabelIdent(label) + mixName(label) case If(cond, thenp, elsep) => mixTag(TagIf) @@ -206,6 +206,13 @@ object Hashers { mixTree(elsep) mixType(tree.tpe) + case LinkTimeIf(cond, thenp, elsep) => + mixTag(TagLinkTimeIf) + mixTree(cond) + mixTree(thenp) + mixTree(elsep) + mixType(tree.tpe) + case While(cond, body) => mixTag(TagWhile) mixTree(cond) @@ -232,10 +239,6 @@ object Hashers { mixTree(finalizer) mixType(tree.tpe) - case Throw(expr) => - mixTag(TagThrow) - mixTree(expr) - case Match(selector, cases, default) => mixTag(TagMatch) mixTree(selector) @@ -246,6 +249,10 @@ object Hashers { mixTree(default) mixType(tree.tpe) + case JSAwait(arg) => + mixTag(TagJSAwait) + mixTree(arg) + case Debugger() => mixTag(TagDebugger) @@ -310,6 +317,24 @@ object Hashers { mixMethodIdent(method) mixTrees(args) + case ApplyTypedClosure(flags, fun, args) => + mixTag(TagApplyTypedClosure) + mixInt(ApplyFlags.toBits(flags)) + mixTree(fun) + mixTrees(args) + + case NewLambda(descriptor, fun) => + val NewLambda.Descriptor(superClass, interfaces, methodName, paramTypes, resultType) = + descriptor + mixTag(TagNewLambda) + mixName(superClass) + mixNames(interfaces) + mixMethodName(methodName) + mixTypes(paramTypes) + mixType(resultType) + mixTree(fun) + mixType(tree.tpe) + case UnaryOp(op, lhs) => mixTag(TagUnaryOp) mixInt(op) @@ -331,10 +356,6 @@ object Hashers { mixArrayTypeRef(typeRef) mixTrees(elems) - case ArrayLength(array) => - mixTag(TagArrayLength) - mixTree(array) - case ArraySelect(array, index) => mixTag(TagArraySelect) mixTree(array) @@ -362,26 +383,6 @@ object Hashers { mixTree(expr) mixType(tpe) - case GetClass(expr) => - mixTag(TagGetClass) - mixTree(expr) - - case Clone(expr) => - mixTag(TagClone) - mixTree(expr) - - case IdentityHashCode(expr) => - mixTag(TagIdentityHashCode) - mixTree(expr) - - case WrapAsThrowable(expr) => - mixTag(TagWrapAsThrowable) - mixTree(expr) - - case UnwrapFromThrowable(expr) => - mixTag(TagUnwrapFromThrowable) - mixTree(expr) - case JSNew(ctor, args) => mixTag(TagJSNew) mixTree(ctor) @@ -478,9 +479,6 @@ object Hashers { mixTag(TagJSTypeOfGlobalRef) mixTree(globalRef) - case JSLinkingInfo() => - mixTag(TagJSLinkingInfo) - case Undefined() => mixTag(TagUndefined) @@ -527,21 +525,30 @@ object Hashers { mixTag(TagClassOf) mixTypeRef(typeRef) - case VarRef(ident) => - mixTag(TagVarRef) - mixLocalIdent(ident) - mixType(tree.tpe) - - case This() => - mixTag(TagThis) + case VarRef(name) => + if (name.isThis) { + // "Optimized" representation, like in Serializers + mixTag(TagThis) + } else { + mixTag(TagVarRef) + mixName(name) + } mixType(tree.tpe) - case Closure(arrow, captureParams, params, restParam, body, captureValues) => + case Closure(flags, captureParams, params, restParam, resultType, body, captureValues) => mixTag(TagClosure) - mixBoolean(arrow) + mixByte(ClosureFlags.toBits(flags).toByte) mixParamDefs(captureParams) mixParamDefs(params) - restParam.foreach(mixParamDef(_)) + if (flags.typed) { + if (restParam.isDefined) + throw new InvalidIRException(tree, "Cannot hash a typed closure with a rest param") + mixType(resultType) + } else { + if (resultType != AnyType) + throw new InvalidIRException(tree, "Cannot hash a JS closure with a result type != AnyType") + restParam.foreach(mixParamDef(_)) + } mixTree(body) mixTrees(captureValues) @@ -550,6 +557,11 @@ object Hashers { mixName(className) mixTrees(captureValues) + case LinkTimeProperty(name) => + mixTag(TagLinkTimeProperty) + mixString(name) + mixType(tree.tpe) + case Transient(value) => throw new InvalidIRException(tree, "Cannot hash a transient IR node (its value is of class " + @@ -579,7 +591,7 @@ object Hashers { def mixTypeRef(typeRef: TypeRef): Unit = typeRef match { case PrimRef(tpe) => tpe match { - case NoType => mixTag(TagVoidRef) + case VoidType => mixTag(TagVoidRef) case BooleanType => mixTag(TagBooleanRef) case CharType => mixTag(TagCharRef) case ByteType => mixTag(TagByteRef) @@ -597,6 +609,10 @@ object Hashers { case typeRef: ArrayTypeRef => mixTag(TagArrayTypeRef) mixArrayTypeRef(typeRef) + case TransientTypeRef(name) => + mixTag(TagTransientTypeRefHashingOnly) + mixName(name) + // The `tpe` is intentionally ignored here; see doc of `TransientTypeRef`. } def mixArrayTypeRef(arrayTypeRef: ArrayTypeRef): Unit = { @@ -619,7 +635,7 @@ object Hashers { case DoubleType => mixTag(TagDoubleType) case StringType => mixTag(TagStringType) case NullType => mixTag(TagNullType) - case NoType => mixTag(TagNoType) + case VoidType => mixTag(TagVoidType) case ClassType(className, nullable) => mixTag(if (nullable) TagClassType else TagNonNullClassType) @@ -629,6 +645,11 @@ object Hashers { mixTag(if (nullable) TagArrayType else TagNonNullArrayType) mixArrayTypeRef(arrayTypeRef) + case ClosureType(paramTypes, resultType, nullable) => + mixTag(if (nullable) TagClosureType else TagNonNullClosureType) + mixTypes(paramTypes) + mixType(resultType) + case RecordType(fields) => mixTag(TagRecordType) for (RecordType.Field(name, originalName, tpe, mutable) <- fields) { @@ -639,12 +660,10 @@ object Hashers { } } - def mixLocalIdent(ident: LocalIdent): Unit = { - mixPos(ident.pos) - mixName(ident.name) - } + def mixTypes(tpes: List[Type]): Unit = + tpes.foreach(mixType) - def mixLabelIdent(ident: LabelIdent): Unit = { + def mixLocalIdent(ident: LocalIdent): Unit = { mixPos(ident.pos) mixName(ident.name) } @@ -674,6 +693,11 @@ object Hashers { def mixName(name: Name): Unit = mixBytes(name.encoded.bytes) + def mixNames(names: List[Name]): Unit = { + mixInt(names.size) + names.foreach(mixName(_)) + } + def mixMethodName(name: MethodName): Unit = { mixName(name.simpleName) mixInt(name.paramTypeRefs.size) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/InvalidIRException.scala b/ir/shared/src/main/scala/org/scalajs/ir/InvalidIRException.scala index f481798458..2e8a272388 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/InvalidIRException.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/InvalidIRException.scala @@ -12,5 +12,12 @@ package org.scalajs.ir -class InvalidIRException(val tree: Trees.IRNode, message: String) - extends Exception(message) +class InvalidIRException(val optTree: Option[Trees.IRNode], message: String) + extends Exception(message) { + + def this(tree: Trees.IRNode, message: String) = + this(Some(tree), message) + + def this(message: String) = + this(None, message) +} diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Names.scala b/ir/shared/src/main/scala/org/scalajs/ir/Names.scala index 73ea0c8c7d..685949052f 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Names.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Names.scala @@ -64,6 +64,10 @@ object Names { * * Local names must be non-empty, and can contain any Unicode code point * except `/ . ; [`. + * + * As an exception, the local name `".this"` represents the `this` binding. + * It cannot be used to declare variables (in `VarDef`s or `ParamDef`s) but + * can be referred to with a `VarRef`. */ final class LocalName private (encoded: UTF8String) extends Name(encoded) with Comparable[LocalName] { @@ -79,22 +83,40 @@ object Names { protected def stringPrefix: String = "LocalName" - final def withPrefix(prefix: LocalName): LocalName = + final def isThis: Boolean = + this eq LocalName.This + + final def withPrefix(prefix: LocalName): LocalName = { + require(!isThis && !prefix.isThis, "cannot concatenate LocalName.This") new LocalName(prefix.encoded ++ this.encoded) + } final def withPrefix(prefix: String): LocalName = LocalName(UTF8String(prefix) ++ this.encoded) - final def withSuffix(suffix: LocalName): LocalName = + final def withSuffix(suffix: LocalName): LocalName = { + require(!isThis && !suffix.isThis, "cannot concatenate LocalName.This") new LocalName(this.encoded ++ suffix.encoded) + } final def withSuffix(suffix: String): LocalName = LocalName(this.encoded ++ UTF8String(suffix)) } object LocalName { - def apply(name: UTF8String): LocalName = - new LocalName(validateSimpleEncodedName(name)) + private final val ThisEncodedName: UTF8String = + UTF8String(".this") + + /** The unique `LocalName` with encoded name `.this`. */ + val This: LocalName = + new LocalName(ThisEncodedName) + + def apply(name: UTF8String): LocalName = { + if (UTF8String.equals(name, ThisEncodedName)) + This + else + new LocalName(validateSimpleEncodedName(name)) + } def apply(name: String): LocalName = LocalName(UTF8String(name)) @@ -393,7 +415,7 @@ object Names { def appendTypeRef(typeRef: TypeRef): Unit = typeRef match { case PrimRef(tpe) => tpe match { - case NoType => builder.append('V') + case VoidType => builder.append('V') case BooleanType => builder.append('Z') case CharType => builder.append('C') case ByteType => builder.append('B') @@ -414,6 +436,8 @@ object Names { i += 1 } appendTypeRef(base) + case TransientTypeRef(name) => + builder.append('t').append(name.nameString) } builder.append(simpleName.nameString) @@ -449,9 +473,6 @@ object Names { } object MethodName { - private val ReflectiveProxyResultTypeRef = ClassRef(ObjectClass) - private final val ReflectiveProxyResultTypeName = "java.lang.Object" - def apply(simpleName: SimpleMethodName, paramTypeRefs: List[TypeRef], resultTypeRef: TypeRef, isReflectiveProxy: Boolean): MethodName = { if ((simpleName.isConstructor || simpleName.isStaticInitializer || @@ -459,11 +480,18 @@ object Names { throw new IllegalArgumentException( "A constructor or static initializer must have a void result type") } - if (isReflectiveProxy && resultTypeRef != ReflectiveProxyResultTypeRef) { - throw new IllegalArgumentException( - "A reflective proxy must have a result type of " + - ReflectiveProxyResultTypeName) + + if (isReflectiveProxy) { + /* It is fine to use WellKnownNames here because nothing in `Names` + * nor `Types` ever creates a reflective proxy name. So this code path + * is not reached during their initialization. + */ + if (resultTypeRef != WellKnownNames.ObjectRef) { + throw new IllegalArgumentException( + "A reflective proxy must have a result type of java.lang.Object") + } } + new MethodName(simpleName, paramTypeRefs, resultTypeRef, isReflectiveProxy) } @@ -487,7 +515,11 @@ object Names { def reflectiveProxy(simpleName: SimpleMethodName, paramTypeRefs: List[TypeRef]): MethodName = { - apply(simpleName, paramTypeRefs, ReflectiveProxyResultTypeRef, + /* It is fine to use WellKnownNames here because nothing in `Names` + * nor `Types` ever creates a reflective proxy name. So this code path + * is not reached during their initialization. + */ + apply(simpleName, paramTypeRefs, WellKnownNames.ObjectRef, isReflectiveProxy = true) } @@ -531,121 +563,6 @@ object Names { // scalastyle:on equals.hash.code - /** `java.lang.Object`, the root of the class hierarchy. */ - val ObjectClass: ClassName = ClassName("java.lang.Object") - - // Hijacked classes - val BoxedUnitClass: ClassName = ClassName("java.lang.Void") - val BoxedBooleanClass: ClassName = ClassName("java.lang.Boolean") - val BoxedCharacterClass: ClassName = ClassName("java.lang.Character") - val BoxedByteClass: ClassName = ClassName("java.lang.Byte") - val BoxedShortClass: ClassName = ClassName("java.lang.Short") - val BoxedIntegerClass: ClassName = ClassName("java.lang.Integer") - val BoxedLongClass: ClassName = ClassName("java.lang.Long") - val BoxedFloatClass: ClassName = ClassName("java.lang.Float") - val BoxedDoubleClass: ClassName = ClassName("java.lang.Double") - val BoxedStringClass: ClassName = ClassName("java.lang.String") - - /** The set of all hijacked classes. */ - val HijackedClasses: Set[ClassName] = Set( - BoxedUnitClass, - BoxedBooleanClass, - BoxedCharacterClass, - BoxedByteClass, - BoxedShortClass, - BoxedIntegerClass, - BoxedLongClass, - BoxedFloatClass, - BoxedDoubleClass, - BoxedStringClass - ) - - /** The class of things returned by `ClassOf` and `GetClass`. */ - val ClassClass: ClassName = ClassName("java.lang.Class") - - /** `java.lang.Cloneable`, which is an ancestor of array classes and is used - * by `Clone`. - */ - val CloneableClass: ClassName = ClassName("java.lang.Cloneable") - - /** `java.io.Serializable`, which is an ancestor of array classes. */ - val SerializableClass: ClassName = ClassName("java.io.Serializable") - - /** The superclass of all throwables. - * - * This is the result type of `WrapAsThrowable` nodes, as well as the input - * type of `UnwrapFromThrowable`. - */ - val ThrowableClass = ClassName("java.lang.Throwable") - - /** The exception thrown by a division by 0. */ - val ArithmeticExceptionClass: ClassName = - ClassName("java.lang.ArithmeticException") - - /** The exception thrown by an `ArraySelect` that is out of bounds. */ - val ArrayIndexOutOfBoundsExceptionClass: ClassName = - ClassName("java.lang.ArrayIndexOutOfBoundsException") - - /** The exception thrown by an `Assign(ArraySelect, ...)` where the value cannot be stored. */ - val ArrayStoreExceptionClass: ClassName = - ClassName("java.lang.ArrayStoreException") - - /** The exception thrown by a `NewArray(...)` with a negative size. */ - val NegativeArraySizeExceptionClass: ClassName = - ClassName("java.lang.NegativeArraySizeException") - - /** The exception thrown by a variety of nodes for `null` arguments. - * - * - `Apply` and `ApplyStatically` for the receiver, - * - `Select` for the qualifier, - * - `ArrayLength` and `ArraySelect` for the array, - * - `GetClass`, `Clone` and `UnwrapFromException` for their respective only arguments. - */ - val NullPointerExceptionClass: ClassName = - ClassName("java.lang.NullPointerException") - - /** The exception thrown by a `BinaryOp.String_charAt` that is out of bounds. */ - val StringIndexOutOfBoundsExceptionClass: ClassName = - ClassName("java.lang.StringIndexOutOfBoundsException") - - /** The exception thrown by an `AsInstanceOf` that fails. */ - val ClassCastExceptionClass: ClassName = - ClassName("java.lang.ClassCastException") - - /** The exception thrown by a `Class_newArray` if the first argument is `classOf[Unit]`. */ - val IllegalArgumentExceptionClass: ClassName = - ClassName("java.lang.IllegalArgumentException") - - /** The set of classes and interfaces that are ancestors of array classes. */ - private[ir] val AncestorsOfPseudoArrayClass: Set[ClassName] = { - /* This would logically be defined in Types, but that introduces a cyclic - * dependency between the initialization of Names and Types. - */ - Set(ObjectClass, CloneableClass, SerializableClass) - } - - /** Name of a constructor without argument. - * - * This is notably the signature of constructors of module classes. - */ - final val NoArgConstructorName: MethodName = - MethodName.constructor(Nil) - - /** This is used to construct a java.lang.Class. */ - final val ObjectArgConstructorName: MethodName = - MethodName.constructor(List(ClassRef(ObjectClass))) - - /** Name of the static initializer method. */ - final val StaticInitializerName: MethodName = - MethodName(SimpleMethodName.StaticInitializer, Nil, VoidRef) - - /** Name of the class initializer method. */ - final val ClassInitializerName: MethodName = - MethodName(SimpleMethodName.ClassInitializer, Nil, VoidRef) - - /** ModuleID of the default module */ - final val DefaultModuleID: String = "main" - // --------------------------------------------------- // ----- Private helpers for validation of names ----- // --------------------------------------------------- diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala index 34f5743e69..facd69b122 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -77,9 +77,23 @@ object Printers { print(end) } + protected final def printRow(ts: List[Type], start: String, sep: String, + end: String)(implicit dummy: DummyImplicit): Unit = { + print(start) + var rest = ts + while (rest.nonEmpty) { + print(rest.head) + rest = rest.tail + if (rest.nonEmpty) + print(sep) + } + print(end) + } + protected def printBlock(tree: Tree): Unit = { val trees = tree match { case Block(trees) => trees + case Skip() => Nil case _ => tree :: Nil } printBlock(trees) @@ -106,7 +120,7 @@ object Printers { print(")") - if (resultType != NoType) { + if (resultType != VoidType) { print(": ") print(resultType) print(" = ") @@ -122,7 +136,6 @@ object Printers { def printAnyNode(node: IRNode): Unit = { node match { case node: LocalIdent => print(node) - case node: LabelIdent => print(node) case node: SimpleFieldIdent => print(node) case node: FieldIdent => print(node) case node: MethodIdent => print(node) @@ -173,7 +186,7 @@ object Printers { case Labeled(label, tpe, body) => print(label) - if (tpe != NoType) { + if (tpe != VoidType) { print('[') print(tpe) print(']') @@ -189,8 +202,10 @@ object Printers { case Return(expr, label) => print("return@") print(label) - print(" ") - print(expr) + if (!expr.isInstanceOf[Skip]) { + print(" ") + print(expr) + } case If(cond, BooleanLiteral(true), elsep) => print(cond) @@ -218,6 +233,14 @@ object Printers { printBlock(elsep) } + case LinkTimeIf(cond, thenp, elsep) => + print("link-time-if (") + print(cond) + print(") ") + printBlock(thenp) + print(" else ") + printBlock(elsep) + case While(cond, body) => print("while (") print(cond) @@ -259,10 +282,6 @@ object Printers { print(" finally ") printBlock(finalizer) - case Throw(expr) => - print("throw ") - print(expr) - case Match(selector, cases, default) => print("match (") print(selector) @@ -281,6 +300,11 @@ object Printers { undent() undent(); println(); print('}') + case JSAwait(arg) => + print("await(") + print(arg) + print(")") + case Debugger() => print("debugger") @@ -343,43 +367,89 @@ object Printers { print(method) printArgs(args) + case ApplyTypedClosure(flags, fun, args) => + print(fun) + printArgs(args) + + case NewLambda(descriptor, fun) => + val NewLambda.Descriptor(superClass, interfaces, methodName, paramTypes, resultType) = + descriptor + + print("("); indent(); println() + + print("extends ") + print(superClass) + if (interfaces.nonEmpty) { + print(" implements ") + print(interfaces.head) + for (intf <- interfaces.tail) { + print(", ") + print(intf) + } + } + print(',') + println() + + print("def ") + print(methodName) + printRow(paramTypes, "(", ", ", "): ") + print(resultType) + print(',') + println() + + print(fun) + + undent(); println(); print(')') + case UnaryOp(op, lhs) => import UnaryOp._ - if (op < String_length) { - print('(') - print((op: @switch) match { - case Boolean_! => - "!" - case IntToChar => - "(char)" - case IntToByte => - "(byte)" - case IntToShort => - "(short)" - case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt => - "(int)" - case IntToLong | DoubleToLong => - "(long)" - case DoubleToFloat | LongToFloat => - "(float)" - case IntToDouble | LongToDouble | FloatToDouble => - "(double)" - }) + def p(prefix: String, suffix: String): Unit = { + print(prefix) print(lhs) - print(')') - } else { - print(lhs) - print((op: @switch) match { - case String_length => ".length" - case CheckNotNull => ".notNull" - case Class_name => ".name" - case Class_isPrimitive => ".isPrimitive" - case Class_isInterface => ".isInterface" - case Class_isArray => ".isArray" - case Class_componentType => ".componentType" - case Class_superClass => ".superClass" - }) + print(suffix) + } + + (op: @switch) match { + case Boolean_! => + p("(!", ")") + case IntToChar => + p("((char)", ")") + case IntToByte => + p("((byte)", ")") + case IntToShort => + p("((short)", ")") + case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt => + p("((int)", ")") + case IntToLong | DoubleToLong => + p("((long)", ")") + case DoubleToFloat | LongToFloat => + p("((float)", ")") + case IntToDouble | LongToDouble | FloatToDouble => + p("((double)", ")") + + case String_length => p("", ".length") + case CheckNotNull => p("", ".notNull") + case Class_name => p("", ".name") + case Class_isPrimitive => p("", ".isPrimitive") + case Class_isInterface => p("", ".isInterface") + case Class_isArray => p("", ".isArray") + case Class_componentType => p("", ".componentType") + case Class_superClass => p("", ".superClass") + case Array_length => p("", ".length") + case GetClass => p("", ".getClass()") + + case Clone => p("(", ")") + case IdentityHashCode => p("(", ")") + case WrapAsThrowable => p("(", ")") + case UnwrapFromThrowable => p("(", ")") + + case Throw => p("throw ", "") + + case Float_toBits => p("(", ")") + case Float_fromBits => p("(", ")") + case Double_toBits => p("(", ")") + case Double_fromBits => p("(", ")") } case BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) => @@ -506,6 +576,11 @@ object Printers { case Double_<= => "<=[double]" case Double_> => ">[double]" case Double_>= => ">=[double]" + + case Int_unsigned_/ => "unsigned_/[int]" + case Int_unsigned_% => "unsigned_%[int]" + case Long_unsigned_/ => "unsigned_/[long]" + case Long_unsigned_% => "unsigned_%[long]" }) print(' ') print(rhs) @@ -524,10 +599,6 @@ object Printers { print(typeRef) printArgs(elems) - case ArrayLength(array) => - print(array) - print(".length") - case ArraySelect(array, index) => print(array) print('[') @@ -563,30 +634,6 @@ object Printers { print(tpe) print(']') - case GetClass(expr) => - print(expr) - print(".getClass()") - - case Clone(expr) => - print("(") - print(expr) - print(')') - - case IdentityHashCode(expr) => - print("(") - print(expr) - print(')') - - case WrapAsThrowable(expr) => - print("(") - print(expr) - print(")") - - case UnwrapFromThrowable(expr) => - print("(") - print(expr) - print(")") - // JavaScript expressions case JSNew(ctor, args) => @@ -594,7 +641,6 @@ object Printers { case JSPrivateSelect(qual, _) => containsOnlySelectsFromAtom(qual) case JSSelect(qual, _) => containsOnlySelectsFromAtom(qual) case VarRef(_) => true - case This() => true case _ => false // in particular, Apply } if (containsOnlySelectsFromAtom(ctor)) { @@ -778,13 +824,10 @@ object Printers { print(globalRef) print(")") - case JSLinkingInfo() => - print("") - // Literals case Undefined() => - print("(void 0)") + print("undefined") case Null() => print("null") @@ -870,17 +913,23 @@ object Printers { // Atomic expressions - case VarRef(ident) => - print(ident) - - case This() => - print("this") - - case Closure(arrow, captureParams, params, restParam, body, captureValues) => - if (arrow) - print("(arrow-lambda<") + case VarRef(name) => + if (name.isThis) + print("this") else - print("(lambda<") + print(name) + + case Closure(flags, captureParams, params, restParam, resultType, body, captureValues) => + print("(") + if (flags.async) + print("async ") + if (flags.typed) + print("typed-lambda") + else if (flags.arrow) + print("arrow-lambda") + else + print("lambda") + print("<") var first = true for ((param, value) <- captureParams.zip(captureValues)) { if (first) @@ -892,7 +941,7 @@ object Printers { print(value) } print(">") - printSig(params, restParam, AnyType) + printSig(params, restParam, resultType) printBlock(body) print(')') @@ -901,6 +950,11 @@ object Printers { print(className) printRow(captureValues, "](", ", ", ")") + case LinkTimeProperty(name) => + print("(") + print(name) + print(")") + // Transient case Transient(value) => @@ -1035,7 +1089,7 @@ object Printers { print(flags.namespace.prefixString) print("set ") printJSMemberName(name) - printSig(arg :: Nil, None, NoType) + printSig(arg :: Nil, None, VoidType) printBlock(body) } @@ -1085,13 +1139,15 @@ object Printers { print(base) for (i <- 1 to dims) print("[]") + case TransientTypeRef(name) => + print(name) } def print(tpe: Type): Unit = tpe match { case AnyType => print("any") case AnyNotNullType => print("any!") case NothingType => print("nothing") - case UndefType => print("void") + case UndefType => print("undef") case BooleanType => print("boolean") case CharType => print("char") case ByteType => print("byte") @@ -1102,7 +1158,7 @@ object Printers { case DoubleType => print("double") case StringType => print("string") case NullType => print("null") - case NoType => print("") + case VoidType => print("void") case ClassType(className, nullable) => print(className) @@ -1114,6 +1170,13 @@ object Printers { if (!nullable) print("!") + case ClosureType(paramTypes, resultType, nullable) => + printRow(paramTypes, "((", ", ", ") => ") + print(resultType) + print(')') + if (!nullable) + print('!') + case RecordType(fields) => print('(') var first = true @@ -1134,9 +1197,6 @@ object Printers { def print(ident: LocalIdent): Unit = print(ident.name) - def print(ident: LabelIdent): Unit = - print(ident.name) - def print(ident: SimpleFieldIdent): Unit = print(ident.name) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index 97162b8bed..23292cbcdc 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,8 +17,8 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.17.0", - binaryEmitted = "1.17" + current = "1.20.0-SNAPSHOT", + binaryEmitted = "1.20-SNAPSHOT" ) /** Helper class to allow for testing of logic. */ diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala index ae2eec6b1f..628630dfa1 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -25,9 +25,11 @@ import Names._ import OriginalName.NoOriginalName import Position._ import Trees._ +import LinkTimeProperty.{ProductionMode, ESVersion, UseECMAScript2015Semantics, IsWebAssembly, LinkerVersion} import Types._ import Tags._ import Version.Unversioned +import WellKnownNames._ import Utils.JumpBackByteArrayOutputStream @@ -40,6 +42,18 @@ object Serializers { */ final val IRMagicNumber = 0xCAFE4A53 + /** A regex for a compatible stable binary IR version from which we may need + * to migrate things with hacks. + */ + private val CompatibleStableIRVersionRegex = { + val prefix = java.util.regex.Pattern.quote(ScalaJSVersions.binaryCross + ".") + new scala.util.matching.Regex(prefix + "(\\d+)") + } + + // For deserialization hack + private final val DynamicImportThunkClass = + ClassName("scala.scalajs.runtime.DynamicImportThunk") + def serialize(stream: OutputStream, classDef: ClassDef): Unit = { new Serializer().serialize(stream, classDef) } @@ -116,24 +130,24 @@ object Serializers { } private final class Serializer { - private[this] val bufferUnderlying = new JumpBackByteArrayOutputStream - private[this] val buffer = new DataOutputStream(bufferUnderlying) + private val bufferUnderlying = new JumpBackByteArrayOutputStream + private val buffer = new DataOutputStream(bufferUnderlying) - private[this] val files = mutable.ListBuffer.empty[URI] - private[this] val fileIndexMap = mutable.Map.empty[URI, Int] + private val files = mutable.ListBuffer.empty[URI] + private val fileIndexMap = mutable.Map.empty[URI, Int] private def fileToIndex(file: URI): Int = fileIndexMap.getOrElseUpdate(file, (files += file).size - 1) - private[this] val encodedNames = mutable.ListBuffer.empty[UTF8String] - private[this] val encodedNameIndexMap = mutable.Map.empty[EncodedNameKey, Int] + private val encodedNames = mutable.ListBuffer.empty[UTF8String] + private val encodedNameIndexMap = mutable.Map.empty[EncodedNameKey, Int] private def encodedNameToIndex(encoded: UTF8String): Int = { val byteString = new EncodedNameKey(encoded) encodedNameIndexMap.getOrElseUpdate(byteString, (encodedNames += encoded).size - 1) } - private[this] val methodNames = mutable.ListBuffer.empty[MethodName] - private[this] val methodNameIndexMap = mutable.Map.empty[MethodName, Int] + private val methodNames = mutable.ListBuffer.empty[MethodName] + private val methodNameIndexMap = mutable.Map.empty[MethodName, Int] private def methodNameToIndex(methodName: MethodName): Int = { methodNameIndexMap.getOrElseUpdate(methodName, { // need to reserve the internal simple names @@ -145,6 +159,8 @@ object Serializers { encodedNameToIndex(className.encoded) case ArrayTypeRef(base, _) => reserveTypeRef(base) + case typeRef: TransientTypeRef => + throw new InvalidIRException(s"Cannot serialize a transient type ref: $typeRef") } encodedNameToIndex(methodName.simpleName.encoded) @@ -154,12 +170,12 @@ object Serializers { }) } - private[this] val strings = mutable.ListBuffer.empty[String] - private[this] val stringIndexMap = mutable.Map.empty[String, Int] + private val strings = mutable.ListBuffer.empty[String] + private val stringIndexMap = mutable.Map.empty[String, Int] private def stringToIndex(str: String): Int = stringIndexMap.getOrElseUpdate(str, (strings += str).size - 1) - private[this] var lastPosition: Position = Position.NoPosition + private var lastPosition: Position = Position.NoPosition def serialize(stream: OutputStream, classDef: ClassDef): Unit = { // Write tree to buffer and record files, names and strings @@ -194,7 +210,7 @@ object Serializers { def writeTypeRef(typeRef: TypeRef): Unit = typeRef match { case PrimRef(tpe) => tpe match { - case NoType => s.writeByte(TagVoidRef) + case VoidType => s.writeByte(TagVoidRef) case BooleanType => s.writeByte(TagBooleanRef) case CharType => s.writeByte(TagCharRef) case ByteType => s.writeByte(TagByteRef) @@ -213,6 +229,13 @@ object Serializers { s.writeByte(TagArrayTypeRef) writeTypeRef(base) s.writeInt(dimensions) + case typeRef: TransientTypeRef => + throw new InvalidIRException(s"Cannot serialize a transient type ref: $typeRef") + } + + def writeTypeRefs(typeRefs: List[TypeRef]): Unit = { + s.writeInt(typeRefs.size) + typeRefs.foreach(writeTypeRef(_)) } // Emit the method names @@ -220,8 +243,7 @@ object Serializers { methodNames.foreach { methodName => s.writeInt(encodedNameIndexMap( new EncodedNameKey(methodName.simpleName.encoded))) - s.writeInt(methodName.paramTypeRefs.size) - methodName.paramTypeRefs.foreach(writeTypeRef(_)) + writeTypeRefs(methodName.paramTypeRefs) writeTypeRef(methodName.resultTypeRef) s.writeBoolean(methodName.isReflectiveProxy) writeName(methodName.simpleName) @@ -260,7 +282,7 @@ object Serializers { case Labeled(label, tpe, body) => writeTagAndPos(TagLabeled) - writeLabelIdent(label); writeType(tpe); writeTree(body) + writeName(label); writeType(tpe); writeTree(body) case Assign(lhs, rhs) => writeTagAndPos(TagAssign) @@ -268,13 +290,18 @@ object Serializers { case Return(expr, label) => writeTagAndPos(TagReturn) - writeTree(expr); writeLabelIdent(label) + writeTree(expr); writeName(label) case If(cond, thenp, elsep) => writeTagAndPos(TagIf) writeTree(cond); writeTree(thenp); writeTree(elsep) writeType(tree.tpe) + case LinkTimeIf(cond, thenp, elsep) => + writeTagAndPos(TagLinkTimeIf) + writeTree(cond); writeTree(thenp); writeTree(elsep) + writeType(tree.tpe) + case While(cond, body) => writeTagAndPos(TagWhile) writeTree(cond); writeTree(body) @@ -294,10 +321,6 @@ object Serializers { writeTagAndPos(TagTryFinally) writeTree(block); writeTree(finalizer) - case Throw(expr) => - writeTagAndPos(TagThrow) - writeTree(expr) - case Match(selector, cases, default) => writeTagAndPos(TagMatch) writeTree(selector) @@ -308,6 +331,10 @@ object Serializers { writeTree(default) writeType(tree.tpe) + case JSAwait(arg) => + writeTagAndPos(TagJSAwait) + writeTree(arg) + case Debugger() => writeTagAndPos(TagDebugger) @@ -355,6 +382,22 @@ object Serializers { writeTagAndPos(TagApplyDynamicImport) writeApplyFlags(flags); writeName(className); writeMethodIdent(method); writeTrees(args) + case ApplyTypedClosure(flags, fun, args) => + writeTagAndPos(TagApplyTypedClosure) + writeApplyFlags(flags); writeTree(fun); writeTrees(args) + + case NewLambda(descriptor, fun) => + val NewLambda.Descriptor(superClass, interfaces, methodName, paramTypes, resultType) = + descriptor + writeTagAndPos(TagNewLambda) + writeName(superClass) + writeNames(interfaces) + writeMethodName(methodName) + writeTypes(paramTypes) + writeType(resultType) + writeTree(fun) + writeType(tree.tpe) + case UnaryOp(op, lhs) => writeTagAndPos(TagUnaryOp) writeByte(op); writeTree(lhs) @@ -372,10 +415,6 @@ object Serializers { writeTagAndPos(TagArrayValue) writeArrayTypeRef(tpe); writeTrees(elems) - case ArrayLength(array) => - writeTagAndPos(TagArrayLength) - writeTree(array) - case ArraySelect(array, index) => writeTagAndPos(TagArraySelect) writeTree(array); writeTree(index) @@ -398,26 +437,6 @@ object Serializers { writeTagAndPos(TagAsInstanceOf) writeTree(expr); writeType(tpe) - case GetClass(expr) => - writeTagAndPos(TagGetClass) - writeTree(expr) - - case Clone(expr) => - writeTagAndPos(TagClone) - writeTree(expr) - - case IdentityHashCode(expr) => - writeTagAndPos(TagIdentityHashCode) - writeTree(expr) - - case WrapAsThrowable(expr) => - writeTagAndPos(TagWrapAsThrowable) - writeTree(expr) - - case UnwrapFromThrowable(expr) => - writeTagAndPos(TagUnwrapFromThrowable) - writeTree(expr) - case JSNew(ctor, args) => writeTagAndPos(TagJSNew) writeTree(ctor); writeTreeOrJSSpreads(args) @@ -500,9 +519,6 @@ object Serializers { writeTagAndPos(TagJSTypeOfGlobalRef) writeTree(globalRef) - case JSLinkingInfo() => - writeTagAndPos(TagJSLinkingInfo) - case Undefined() => writeTagAndPos(TagUndefined) @@ -549,21 +565,33 @@ object Serializers { writeTagAndPos(TagClassOf) writeTypeRef(typeRef) - case VarRef(ident) => - writeTagAndPos(TagVarRef) - writeLocalIdent(ident) - writeType(tree.tpe) - - case This() => - writeTagAndPos(TagThis) + case VarRef(name) => + if (name.isThis) { + // "Optimized" representation that is compatible with IR < 1.18 + writeTagAndPos(TagThis) + } else { + writeTagAndPos(TagVarRef) + writeName(name) + } writeType(tree.tpe) - case Closure(arrow, captureParams, params, restParam, body, captureValues) => + case Closure(flags, captureParams, params, restParam, resultType, body, captureValues) => writeTagAndPos(TagClosure) - writeBoolean(arrow) + writeClosureFlags(flags) writeParamDefs(captureParams) writeParamDefs(params) - writeOptParamDef(restParam) + + // Compatible with IR < v1.19, which had no `resultType` + if (flags.typed) { + if (restParam.isDefined) + throw new InvalidIRException(tree, "Cannot serialize a typed closure with a rest param") + writeType(resultType) + } else { + if (resultType != AnyType) + throw new InvalidIRException(tree, "Cannot serialize a JS closure with a result type != AnyType") + writeOptParamDef(restParam) + } + writeTree(body) writeTrees(captureValues) @@ -572,6 +600,11 @@ object Serializers { writeName(className) writeTrees(captureValues) + case LinkTimeProperty(name) => + writeTagAndPos(TagLinkTimeProperty) + writeString(name) + writeType(tree.tpe) + case Transient(value) => throw new InvalidIRException(tree, "Cannot serialize a transient IR node (its value is of class " + @@ -779,11 +812,6 @@ object Serializers { writeName(ident.name) } - def writeLabelIdent(ident: LabelIdent): Unit = { - writePosition(ident.pos) - writeName(ident.name) - } - def writeSimpleFieldIdent(ident: SimpleFieldIdent): Unit = { writePosition(ident.pos) writeName(ident.name) @@ -824,6 +852,11 @@ object Serializers { def writeName(name: Name): Unit = buffer.writeInt(encodedNameToIndex(name.encoded)) + def writeNames(names: List[Name]): Unit = { + buffer.writeInt(names.size) + names.foreach(writeName(_)) + } + def writeMethodName(name: MethodName): Unit = buffer.writeInt(methodNameToIndex(name)) @@ -867,7 +900,7 @@ object Serializers { case DoubleType => buffer.write(TagDoubleType) case StringType => buffer.write(TagStringType) case NullType => buffer.write(TagNullType) - case NoType => buffer.write(TagNoType) + case VoidType => buffer.write(TagVoidType) case ClassType(className, nullable) => buffer.write(if (nullable) TagClassType else TagNonNullClassType) @@ -877,6 +910,11 @@ object Serializers { buffer.write(if (nullable) TagArrayType else TagNonNullArrayType) writeArrayTypeRef(arrayTypeRef) + case ClosureType(paramTypes, resultType, nullable) => + buffer.write(if (nullable) TagClosureType else TagNonNullClosureType) + writeTypes(paramTypes) + writeType(resultType) + case RecordType(fields) => buffer.write(TagRecordType) buffer.writeInt(fields.size) @@ -889,10 +927,15 @@ object Serializers { } } + def writeTypes(tpes: List[Type]): Unit = { + buffer.writeInt(tpes.size) + tpes.foreach(writeType) + } + def writeTypeRef(typeRef: TypeRef): Unit = typeRef match { case PrimRef(tpe) => tpe match { - case NoType => buffer.writeByte(TagVoidRef) + case VoidType => buffer.writeByte(TagVoidRef) case BooleanType => buffer.writeByte(TagBooleanRef) case CharType => buffer.writeByte(TagCharRef) case ByteType => buffer.writeByte(TagByteRef) @@ -910,6 +953,8 @@ object Serializers { case typeRef: ArrayTypeRef => buffer.writeByte(TagArrayTypeRef) writeArrayTypeRef(typeRef) + case typeRef: TransientTypeRef => + throw new InvalidIRException(s"Cannot serialize a transient type ref: $typeRef") } def writeArrayTypeRef(typeRef: ArrayTypeRef): Unit = { @@ -917,9 +962,17 @@ object Serializers { buffer.writeInt(typeRef.dimensions) } + def writeTypeRefs(typeRefs: List[TypeRef]): Unit = { + buffer.writeInt(typeRefs.size) + typeRefs.foreach(writeTypeRef(_)) + } + def writeApplyFlags(flags: ApplyFlags): Unit = buffer.writeInt(ApplyFlags.toBits(flags)) + def writeClosureFlags(flags: ClosureFlags): Unit = + buffer.writeByte(ClosureFlags.toBits(flags)) + def writePosition(pos: Position): Unit = { import buffer._ import PositionFormat._ @@ -1013,16 +1066,16 @@ object Serializers { private final class Deserializer(buf: ByteBuffer) { require(buf.order() == ByteOrder.BIG_ENDIAN) - private[this] var hacks: Hacks = _ - private[this] var files: Array[URI] = _ - private[this] var encodedNames: Array[UTF8String] = _ - private[this] var localNames: Array[LocalName] = _ - private[this] var labelNames: Array[LabelName] = _ - private[this] var simpleFieldNames: Array[SimpleFieldName] = _ - private[this] var simpleMethodNames: Array[SimpleMethodName] = _ - private[this] var classNames: Array[ClassName] = _ - private[this] var methodNames: Array[MethodName] = _ - private[this] var strings: Array[String] = _ + private var hacks: Hacks = null + private var files: Array[URI] = null + private var encodedNames: Array[UTF8String] = null + private var localNames: Array[LocalName] = null + private var labelNames: Array[LabelName] = null + private var simpleFieldNames: Array[SimpleFieldName] = null + private var simpleMethodNames: Array[SimpleMethodName] = null + private var classNames: Array[ClassName] = null + private var methodNames: Array[MethodName] = null + private var strings: Array[String] = null /** Uniqueness cache for FieldName's. * @@ -1033,12 +1086,13 @@ object Serializers { * to make them all `eq`, consuming less memory and speeding up equality * tests. */ - private[this] val uniqueFieldNames = mutable.AnyRefMap.empty[FieldName, FieldName] + private val uniqueFieldNames = mutable.AnyRefMap.empty[FieldName, FieldName] - private[this] var lastPosition: Position = Position.NoPosition + private var lastPosition: Position = Position.NoPosition - private[this] var enclosingClassName: ClassName = _ - private[this] var thisTypeForHack: Option[Type] = None + private var enclosingClassName: ClassName = null + private var thisTypeForHack: Option[Type] = None + private var patchDynamicImportThunkSuperCtorCall: Boolean = false def deserializeEntryPointsInfo(): EntryPointsInfo = { hacks = new Hacks(sourceVersion = readHeader()) @@ -1127,17 +1181,17 @@ object Serializers { case TagVarDef => VarDef(readLocalIdent(), readOriginalName(), readType(), readBoolean(), readTree()) case TagSkip => Skip() case TagBlock => Block(readTrees()) - case TagLabeled => Labeled(readLabelIdent(), readType(), readTree()) + case TagLabeled => Labeled(readLabelName(), readType(), readTree()) case TagAssign => val lhs0 = readTree() - val lhs = if (hacks.use4 && lhs0.tpe == NothingType) { + val lhs = if (hacks.useBelow(5) && lhs0.tpe == NothingType) { /* Note [Nothing FieldDef rewrite] * (throw qual.field[null]) = rhs --> qual.field[null] = rhs */ lhs0 match { - case Throw(sel: Select) if sel.tpe == NullType => sel - case _ => lhs0 + case UnaryOp(UnaryOp.Throw, sel: Select) if sel.tpe == NullType => sel + case _ => lhs0 } } else { lhs0 @@ -1147,12 +1201,17 @@ object Serializers { Assign(lhs.asInstanceOf[AssignLhs], rhs) - case TagReturn => Return(readTree(), readLabelIdent()) - case TagIf => If(readTree(), readTree(), readTree())(readType()) - case TagWhile => While(readTree(), readTree()) + case TagReturn => + Return(readTree(), readLabelName()) + case TagIf => + If(readTree(), readTree(), readTree())(readType()) + case TagLinkTimeIf => + LinkTimeIf(readTree(), readTree(), readTree())(readType()) + case TagWhile => + While(readTree(), readTree()) case TagDoWhile => - if (!hacks.use12) + if (!hacks.useBelow(13)) throw new IOException(s"Found invalid pre-1.13 DoWhile loop at $pos") // Rewrite `do { body } while (cond)` to `while ({ body; cond }) {}` val body = readTree() @@ -1167,30 +1226,37 @@ object Serializers { case TagTryFinally => TryFinally(readTree(), readTree()) - case TagThrow => - val expr = readTree() - val patchedExpr = - if (hacks.use8) throwArgumentHack8(expr) - else expr - Throw(patchedExpr) - case TagMatch => Match(readTree(), List.fill(readInt()) { (readTrees().map(_.asInstanceOf[MatchableLiteral]), readTree()) }, readTree())(readType()) + + case TagJSAwait => + JSAwait(readTree()) + case TagDebugger => Debugger() - case TagNew => New(readClassName(), readMethodIdent(), readTrees()) - case TagLoadModule => LoadModule(readClassName()) + case TagNew => + val tree = New(readClassName(), readMethodIdent(), readTrees()) + if (hacks.useBelow(19)) + anonFunctionNewNodeHackBelow19(tree) + else + tree + + case TagLoadModule => + LoadModule(readClassName()) case TagStoreModule => - if (hacks.use13) { + if (hacks.useBelow(16)) { val cls = readClassName() val rhs = readTree() - if (cls != enclosingClassName || !rhs.isInstanceOf[This]) { - throw new IOException( - s"Illegal legacy StoreModule(${cls.nameString}, $rhs) " + - s"found in class ${enclosingClassName.nameString}") + rhs match { + case This() if cls == enclosingClassName => + // ok + case _ => + throw new IOException( + s"Illegal legacy StoreModule(${cls.nameString}, $rhs) " + + s"found in class ${enclosingClassName.nameString}") } } StoreModule() @@ -1200,11 +1266,11 @@ object Serializers { val field = readFieldIdent() val tpe = readType() - if (hacks.use4 && tpe == NothingType) { + if (hacks.useBelow(5) && tpe == NothingType) { /* Note [Nothing FieldDef rewrite] * qual.field[nothing] --> throw qual.field[null] */ - Throw(Select(qualifier, field)(NullType)) + UnaryOp(UnaryOp.Throw, Select(qualifier, field)(NullType)) } else { Select(qualifier, field)(tpe) } @@ -1215,19 +1281,70 @@ object Serializers { case TagApply => Apply(readApplyFlags(), readTree(), readMethodIdent(), readTrees())( readType()) + case TagApplyStatically => - ApplyStatically(readApplyFlags(), readTree(), readClassName(), - readMethodIdent(), readTrees())(readType()) + val flags = readApplyFlags() + val receiver = readTree() + val className0 = readClassName() + val method = readMethodIdent() + val args = readTrees() + val tpe = readType() + + val className = { + if (patchDynamicImportThunkSuperCtorCall && method.name.isConstructor) + DynamicImportThunkClass + else + className0 + } + + ApplyStatically(flags, receiver, className, method, args)(tpe) + case TagApplyStatic => ApplyStatic(readApplyFlags(), readClassName(), readMethodIdent(), readTrees())(readType()) case TagApplyDynamicImport => ApplyDynamicImport(readApplyFlags(), readClassName(), readMethodIdent(), readTrees()) + case TagApplyTypedClosure => + ApplyTypedClosure(readApplyFlags(), readTree(), readTrees()) + case TagNewLambda => + val descriptor = NewLambda.Descriptor(readClassName(), + readClassNames(), readMethodName(), readTypes(), readType()) + NewLambda(descriptor, readTree())(readType()) case TagUnaryOp => UnaryOp(readByte(), readTree()) case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree()) + case TagArrayLength | TagGetClass | TagClone | TagIdentityHashCode | + TagWrapAsThrowable | TagUnwrapFromThrowable | TagThrow => + if (!hacks.useBelow(18)) { + throw new IOException( + s"Illegal legacy node $tag found in class ${enclosingClassName.nameString}") + } + + val lhs = readTree() + def checkNotNullLhs: Tree = UnaryOp(UnaryOp.CheckNotNull, lhs) + + (tag: @switch) match { + case TagArrayLength => + UnaryOp(UnaryOp.Array_length, checkNotNullLhs) + case TagGetClass => + UnaryOp(UnaryOp.GetClass, checkNotNullLhs) + case TagClone => + UnaryOp(UnaryOp.Clone, checkNotNullLhs) + case TagIdentityHashCode => + UnaryOp(UnaryOp.IdentityHashCode, lhs) + case TagWrapAsThrowable => + UnaryOp(UnaryOp.WrapAsThrowable, lhs) + case TagUnwrapFromThrowable => + UnaryOp(UnaryOp.UnwrapFromThrowable, checkNotNullLhs) + case TagThrow => + val patchedLhs = + if (hacks.useBelow(11)) throwArgumentHackBelow11(lhs) + else lhs + UnaryOp(UnaryOp.Throw, patchedLhs) + } + case TagNewArray => val arrayTypeRef = readArrayTypeRef() val lengths = readTrees() @@ -1236,7 +1353,7 @@ object Serializers { NewArray(arrayTypeRef, length) case _ => - if (hacks.use16) { + if (hacks.useBelow(17)) { // Rewrite as a call to j.l.r.Array.newInstance val ArrayTypeRef(base, origDims) = arrayTypeRef val newDims = origDims - lengths.size @@ -1261,14 +1378,13 @@ object Serializers { } case TagArrayValue => ArrayValue(readArrayTypeRef(), readTrees()) - case TagArrayLength => ArrayLength(readTree()) case TagArraySelect => ArraySelect(readTree(), readTree())(readType()) case TagRecordValue => RecordValue(readType().asInstanceOf[RecordType], readTrees()) case TagIsInstanceOf => val expr = readTree() val testType0 = readType() - val testType = if (hacks.use16) { + val testType = if (hacks.useBelow(17)) { testType0 match { case ClassType(className, true) => ClassType(className, nullable = false) case ArrayType(arrayTypeRef, true) => ArrayType(arrayTypeRef, nullable = false) @@ -1281,18 +1397,33 @@ object Serializers { IsInstanceOf(expr, testType) case TagAsInstanceOf => AsInstanceOf(readTree(), readType()) - case TagGetClass => GetClass(readTree()) - case TagClone => Clone(readTree()) - case TagIdentityHashCode => IdentityHashCode(readTree()) - - case TagWrapAsThrowable => - WrapAsThrowable(readTree()) - case TagUnwrapFromThrowable => - UnwrapFromThrowable(readTree()) - - case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads()) - case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent()) - case TagJSSelect => JSSelect(readTree(), readTree()) + + case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads()) + case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent()) + + case TagJSSelect => + if (hacks.useBelow(18) && buf.get(buf.position()) == TagJSLinkingInfo) { + val jsLinkingInfo = readTree() + readTree() match { + case StringLiteral("productionMode") => + LinkTimeProperty(ProductionMode)(BooleanType) + case StringLiteral("esVersion") => + LinkTimeProperty(ESVersion)(IntType) + case StringLiteral("assumingES6") => + LinkTimeProperty(UseECMAScript2015Semantics)(BooleanType) + case StringLiteral("isWebAssembly") => + LinkTimeProperty(IsWebAssembly)(BooleanType) + case StringLiteral("linkerVersion") => + LinkTimeProperty(LinkerVersion)(StringType) + case StringLiteral("fileLevelThis") => + JSGlobalRef(JSGlobalRef.FileLevelThis) + case otherItem => + JSSelect(jsLinkingInfo, otherItem) + } + } else { + JSSelect(readTree(), readTree()) + } + case TagJSFunctionApply => JSFunctionApply(readTree(), readTreeOrJSSpreads()) case TagJSMethodApply => JSMethodApply(readTree(), readTree(), readTreeOrJSSpreads()) case TagJSSuperSelect => JSSuperSelect(readTree(), readTree(), readTree()) @@ -1312,7 +1443,21 @@ object Serializers { JSObjectConstr(List.fill(readInt())((readTree(), readTree()))) case TagJSGlobalRef => JSGlobalRef(readString()) case TagJSTypeOfGlobalRef => JSTypeOfGlobalRef(readTree().asInstanceOf[JSGlobalRef]) - case TagJSLinkingInfo => JSLinkingInfo() + + case TagJSLinkingInfo => + if (hacks.useBelow(18)) { + JSObjectConstr(List( + (StringLiteral("productionMode"), LinkTimeProperty(ProductionMode)(BooleanType)), + (StringLiteral("esVersion"), LinkTimeProperty(ESVersion)(IntType)), + (StringLiteral("assumingES6"), LinkTimeProperty(UseECMAScript2015Semantics)(BooleanType)), + (StringLiteral("isWebAssembly"), LinkTimeProperty(IsWebAssembly)(BooleanType)), + (StringLiteral("linkerVersion"), LinkTimeProperty(LinkerVersion)(StringType)), + (StringLiteral("fileLevelThis"), JSGlobalRef(JSGlobalRef.FileLevelThis)) + )) + } else { + throw new IOException( + s"Found invalid pre-1.18 JSLinkingInfo def at ${pos}") + } case TagUndefined => Undefined() case TagNull => Null() @@ -1328,16 +1473,26 @@ object Serializers { case TagClassOf => ClassOf(readTypeRef()) case TagVarRef => - VarRef(readLocalIdent())(readType()) + val name = + if (hacks.useBelow(18)) readLocalIdent().name + else readLocalName() + VarRef(name)(readType()) case TagThis => val tpe = readType() This()(thisTypeForHack.getOrElse(tpe)) case TagClosure => - val arrow = readBoolean() + val flags = readClosureFlags() val captureParams = readParamDefs() - val (params, restParam) = readParamDefsWithRest() + + val (params, restParam, resultType) = if (flags.typed) { + (readParamDefs(), None, readType()) + } else { + val (params, restParam) = readParamDefsWithRest() + (params, restParam, AnyType) + } + val body = if (thisTypeForHack.isEmpty) { // Fast path; always taken for IR >= 1.17 readTree() @@ -1351,14 +1506,17 @@ object Serializers { } } val captureValues = readTrees() - Closure(arrow, captureParams, params, restParam, body, captureValues) + Closure(flags, captureParams, params, restParam, resultType, body, captureValues) case TagCreateJSClass => CreateJSClass(readClassName(), readTrees()) + + case TagLinkTimeProperty => + LinkTimeProperty(readString())(readType()) } } - /** Patches the argument of a `Throw` for IR version until 1.8. + /** Patches the argument of a `Throw` for IR version below 1.11. * * Prior to Scala.js 1.11, `Throw(e)` was emitted by the compiler with * the somewhat implied assumption that it would "throw an NPE" (but @@ -1373,65 +1531,154 @@ object Serializers { * so that `Throw(e)` only ever throws the value of `e`, while the NPE UB * is specified by `UnwrapFromThrowable`. Among other things, this allows * the user-space code `js.special.throw(e)` to indiscriminately throw `e` - * even if it is `null`. + * even if it is `null`. Later, in Scala.js 1.18, we further separated the + * null check of `UnwrapFromThrowable` to be an explicit `CheckNotNull`. * - * With this hack, we patch `Throw(e)` where `e` is a nullable `Throwable` - * by inserting an appropriate `UnwrapFromThrowable`. + * With this hack, we patch `Throw(e)` by inserting an appropriate + * `CheckNotNull`. * - * Naively, we would just return `UnwrapFromThrowable(e)`. Unfortunately, - * we cannot prove that this is type-correct when the type of `e` is a - * `ClassType(cls)`, as we cannot test whether `cls` is a subclass of - * `java.lang.Throwable`. So we have to produce the following instead: + * However, we must not do that when the previous Scala.js compiler + * already provides the *unwrapped* exception. This happened in two + * situations: * + * - when automatically re-throwing an unhandled exception at the end of a + * `try..catch`, or + * - when throwing a maybe-JavaScriptException, with an explicit call to + * `runtime.package$.unwrapJavaScriptException(x)`. + * + * Fortunately, in both situations, the type of the `expr` is always + * `AnyType`. We can accurately use that test to know whether we need to + * apply the patch. + */ + private def throwArgumentHackBelow11(expr: Tree)(implicit pos: Position): Tree = { + if (expr.tpe == AnyType) + expr + else if (!expr.tpe.isNullable) + expr // no CheckNotNull needed; common case because of `throw new ...` + else + UnaryOp(UnaryOp.CheckNotNull, expr) + } + + /** Rewrites `New` nodes of `AnonFunctionN`s coming from before 1.19 into `NewLambda` nodes. + * + * Before 1.19, the codegen for `scala.FunctionN` lambda was of the following shape: * {{{ - * if (expr === null) unwrapFromThrowable(null) else expr + * new scala.scalajs.runtime.AnonFunctionN(arrow-lambda<...captures>(...args: any): any = { + * body + * }) * }}} * - * except that evaluates `expr` twice. If it is a `VarRef`, which is a - * common case, that is fine. Otherwise, we have to wrap this pattern in - * an IIFE. + * This function rewrites such calls to `NewLambda` nodes, using the new + * definition of these classes: + * {{{ + * (scala.scalajs.runtime.AnonFunctionN, + * apply;Ljava.lang.Object;...;Ljava.lang.Object, + * any, any, (typed-lambda<...captures>(...args: any): any = { + * body + * })) + * }}} * - * We also have to avoid the transformation altogether when the `expr` is - * an `AnyType`. This happens when the previous Scala.js compiler already - * provides the unwrapped exception, which is either + * The rewrite ensures that previously published lambdas get the same + * optimizations on Wasm as those recompiled with 1.19+. * - * - when automatically re-throwing an unhandled exception at the end of a - * `try..catch`, or - * - when throwing a maybe-JavaScriptException, with an explicit call to - * `runtime.package$.unwrapJavaScriptException(x)`. + * The rewrite also applies to Scala 3's `AnonFunctionXXL` classes, with + * an additional adaptation of the parameter's type. It rewrites + * {{{ + * new scala.scalajs.runtime.AnonFunctionXXL(arrow-lambda<...captures>(argArray: any): any = { + * body + * }) + * }}} + * to + * {{{ + * (scala.scalajs.runtime.AnonFunctionXXL, + * apply;Ljava.lang.Object[];Ljava.lang.Object, + * any, any, (typed-lambda<...captures>(argArray: jl.Object[]): any = { + * newBody + * })) + * }}} + * where `newBody` is `body` transformed to adapt the type of `argArray` + * everywhere. + * + * Tests are in `sbt-plugin/src/sbt-test/linker/anonfunction-compat/`. + * + * --- + * + * In case the argument is not an arrow-lambda of the expected shape, we + * use a fallback. This never happens for our published codegens, but + * could happen for other valid IR. We rewrite + * {{{ + * new scala.scalajs.runtime.AnonFunctionN(jsFunctionArg) + * }}} + * to + * {{{ + * (scala.scalajs.runtime.AnonFunctionN, + * apply;Ljava.lang.Object;...;Ljava.lang.Object, + * any, any, (typed-lambda(...args: any): any = { + * f(...args) + * })) + * }}} + * + * This code path is not tested in the CI, but can be locally tested by + * commenting out the `case Closure(...) =>`. */ - private def throwArgumentHack8(expr: Tree)(implicit pos: Position): Tree = { - expr.tpe match { - case NullType => - // Evaluate the expression then definitely run into an NPE UB - UnwrapFromThrowable(expr) - - case ClassType(_, _) => - expr match { - case New(_, _, _) => - // Common case (`throw new SomeException(...)`) that is known not to be `null` - expr - case VarRef(_) => - /* Common case (explicit re-throw of the form `throw th`) where we don't need the IIFE. - * if (expr === null) unwrapFromThrowable(null) else expr - */ - If(BinaryOp(BinaryOp.===, expr, Null()), UnwrapFromThrowable(Null()), expr)(AnyType) + private def anonFunctionNewNodeHackBelow19(tree: New): Tree = { + tree match { + case New(cls, _, funArg :: Nil) => + def makeFallbackTypedClosure(paramTypes: List[Type]): Closure = { + implicit val pos = funArg.pos + val fParamDef = ParamDef(LocalIdent(LocalName("f")), NoOriginalName, AnyType, mutable = false) + val xParamDefs = paramTypes.zipWithIndex.map { case (ptpe, i) => + ParamDef(LocalIdent(LocalName(s"x$i")), NoOriginalName, ptpe, mutable = false) + } + Closure(ClosureFlags.typed, List(fParamDef), xParamDefs, None, AnyType, + JSFunctionApply(fParamDef.ref, xParamDefs.map(_.ref)), + List(funArg)) + } + + cls match { + case HackNames.AnonFunctionClass(arity) => + val typedClosure = funArg match { + // The shape produced by our earlier compilers, which we can optimally rewrite + case Closure(ClosureFlags.arrow, captureParams, params, None, AnyType, body, captureValues) + if params.lengthCompare(arity) == 0 => + Closure(ClosureFlags.typed, captureParams, params, None, AnyType, + body, captureValues)(funArg.pos) + + // Fallback for other shapes (theoretically required; dead code in practice) + case _ => + makeFallbackTypedClosure(List.fill(arity)(AnyType)) + } + + NewLambda(HackNames.anonFunctionDescriptors(arity), typedClosure)(tree.tpe)(tree.pos) + + case HackNames.AnonFunctionXXLClass => + val typedClosure = funArg match { + // The shape produced by our earlier compilers, which we can optimally rewrite + case Closure(ClosureFlags.arrow, captureParams, oldParam :: Nil, None, AnyType, body, captureValues) => + // Here we need to adapt the type of the parameter from `any` to `jl.Object[]`. + val newParam = oldParam.copy(ptpe = HackNames.ObjectArrayType)(oldParam.pos) + val newBody = new Transformers.LocalScopeTransformer { + override def transform(tree: Tree): Tree = tree match { + case tree @ VarRef(newParam.name.name) => tree.copy()(newParam.ptpe)(tree.pos) + case _ => super.transform(tree) + } + }.transform(body) + Closure(ClosureFlags.typed, captureParams, List(newParam), None, AnyType, + newBody, captureValues)(funArg.pos) + + // Fallback for other shapes (theoretically required; dead code in practice) + case _ => + makeFallbackTypedClosure(List(HackNames.ObjectArrayType)) + } + + NewLambda(HackNames.anonFunctionXXLDescriptor, typedClosure)(tree.tpe)(tree.pos) + case _ => - /* General case where we need to avoid evaluating `expr` twice. - * ((x) => if (x === null) unwrapFromThrowable(null) else x)(expr) - */ - val x = LocalIdent(LocalName("x")) - val xParamDef = ParamDef(x, OriginalName.NoOriginalName, AnyType, mutable = false) - val xRef = xParamDef.ref - val closure = Closure(arrow = true, Nil, List(xParamDef), None, { - If(BinaryOp(BinaryOp.===, xRef, Null()), UnwrapFromThrowable(Null()), xRef)(AnyType) - }, Nil) - JSFunctionApply(closure, List(expr)) + tree } case _ => - // Do not transform expressions of other types, in particular `AnyType` - expr + tree } } @@ -1448,11 +1695,11 @@ object Serializers { val originalName = readOriginalName() val kind = ClassKind.fromByte(readByte()) - if (hacks.use16) { + if (hacks.useBelow(17)) { thisTypeForHack = kind match { case ClassKind.Class | ClassKind.ModuleClass | ClassKind.Interface => Some(ClassType(cls, nullable = false)) - case ClassKind.HijackedClass if hacks.use8 => + case ClassKind.HijackedClass if hacks.useBelow(11) => // Use getOrElse as safety guard for otherwise invalid inputs Some(BoxedClassToPrimType.getOrElse(cls, ClassType(cls, nullable = false))) case _ => @@ -1467,7 +1714,16 @@ object Serializers { val superClass = readOptClassIdent() val parents = readClassIdents() - /* jsSuperClass is not hacked like in readMemberDef.bodyHack5. The + if (hacks.useBelow(18) && kind.isClass) { + /* In 1.18, we started enforcing the constructor chaining discipline. + * Unfortunately, we used to generate a wrong super constructor call in + * synthetic classes extending `DynamicImportThunk`, so we patch them. + */ + patchDynamicImportThunkSuperCtorCall = + superClass.exists(_.name == DynamicImportThunkClass) + } + + /* jsSuperClass is not hacked like in readMemberDef.bodyHackBelow6. The * compilers before 1.6 always use a simple VarRef() as jsSuperClass, * when there is one, so no hack is required. */ @@ -1488,7 +1744,7 @@ object Serializers { case TagFieldDef => fieldsBuilder += readFieldDef() case TagJSFieldDef => fieldsBuilder += readJSFieldDef() case TagMethodDef => methodsBuilder += readMethodDef(cls, kind) - case TagJSConstructorDef => jsConstructorBuilder += readJSConstructorDef() + case TagJSConstructorDef => jsConstructorBuilder += readJSConstructorDef(kind) case TagJSMethodDef => jsMethodPropsBuilder += readJSMethodDef() case TagJSPropertyDef => jsMethodPropsBuilder += readJSPropertyDef() case TagJSNativeMemberDef => jsNativeMembersBuilder += readJSNativeMemberDef() @@ -1502,22 +1758,22 @@ object Serializers { val methods = { val methods0 = methodsBuilder.result() - if (hacks.use4 && kind.isJSClass) { + if (hacks.useBelow(5) && kind.isJSClass) { // #4409: Filter out abstract methods in non-native JS classes for version < 1.5 methods0.filter(_.body.isDefined) - } else if (hacks.use16 && cls == ClassClass) { - jlClassMethodsHack16(methods0) - } else if (hacks.use16 && cls == HackNames.ReflectArrayModClass) { - jlReflectArrayMethodsHack16(methods0) + } else if (hacks.useBelow(17) && cls == ClassClass) { + jlClassMethodsHackBelow17(methods0) + } else if (hacks.useBelow(17) && cls == HackNames.ReflectArrayModClass) { + jlReflectArrayMethodsHackBelow17(methods0) } else { methods0 } } val (jsConstructor, jsMethodProps) = { - if (hacks.use8 && kind.isJSClass) { - assert(jsConstructorBuilder.result().isEmpty, "found JSConstructorDef in pre 1.8 IR") - jsConstructorHack(jsMethodPropsBuilder.result()) + if (hacks.useBelow(11) && kind.isJSClass) { + assert(jsConstructorBuilder.result().isEmpty, "found JSConstructorDef in pre 1.11 IR") + jsConstructorHackBelow11(kind, jsMethodPropsBuilder.result()) } else { (jsConstructorBuilder.result(), jsMethodPropsBuilder.result()) } @@ -1525,13 +1781,18 @@ object Serializers { val jsNativeMembers = jsNativeMembersBuilder.result() - ClassDef(name, originalName, kind, jsClassCaptures, superClass, parents, + val classDef = ClassDef(name, originalName, kind, jsClassCaptures, superClass, parents, jsSuperClass, jsNativeLoadSpec, fields, methods, jsConstructor, jsMethodProps, jsNativeMembers, topLevelExportDefs)( optimizerHints) + + if (hacks.useBelow(19)) + anonFunctionClassDefHackBelow19(classDef) + else + classDef } - private def jlClassMethodsHack16(methods: List[MethodDef]): List[MethodDef] = { + private def jlClassMethodsHackBelow17(methods: List[MethodDef]): List[MethodDef] = { for (method <- methods) yield { implicit val pos = method.pos @@ -1543,9 +1804,9 @@ object Serializers { if (methodName.isConstructor) { val newName = MethodIdent(NoArgConstructorName)(method.name.pos) val newBody = ApplyStatically(ApplyFlags.empty.withConstructor(true), - thisJLClass, ObjectClass, newName, Nil)(NoType) + thisJLClass, ObjectClass, newName, Nil)(VoidType) MethodDef(method.flags, newName, method.originalName, - Nil, NoType, Some(newBody))( + Nil, VoidType, Some(newBody))( method.optimizerHints, method.version) } else { def argRef = method.args.head.ref @@ -1573,15 +1834,15 @@ object Serializers { * We must replace those occurrences with a `Class_name` as well. */ val transformer = new Transformers.Transformer { - override def transform(tree: Tree, isStat: Boolean): Tree = tree match { + override def transform(tree: Tree): Tree = tree match { case JSSelect(_, StringLiteral("name")) => implicit val pos = tree.pos UnaryOp(UnaryOp.Class_name, thisJLClass) case _ => - super.transform(tree, isStat) + super.transform(tree) } } - transformer.transform(method.body.get, isStat = method.resultType == NoType) + transformer.transform(method.body.get) } val newOptimizerHints = @@ -1595,7 +1856,7 @@ object Serializers { } } - private def jlReflectArrayMethodsHack16(methods: List[MethodDef]): List[MethodDef] = { + private def jlReflectArrayMethodsHackBelow17(methods: List[MethodDef]): List[MethodDef] = { /* Basically this method hard-codes new implementations for the two * overloads of newInstance. * It is horrible, but better than pollute everything else in the linker. @@ -1611,6 +1872,12 @@ object Serializers { VarDef(LocalIdent(LocalName(name)), NoOriginalName, vtpe, mutable, rhs) } + def arrayLength(t: Tree)(implicit pos: Position): Tree = + UnaryOp(UnaryOp.Array_length, UnaryOp(UnaryOp.CheckNotNull, t)) + + def getClass(t: Tree)(implicit pos: Position): Tree = + UnaryOp(UnaryOp.GetClass, UnaryOp(UnaryOp.CheckNotNull, t)) + val jlClassRef = ClassRef(ClassClass) val intArrayTypeRef = ArrayTypeRef(IntRef, 1) val objectRef = ClassRef(ObjectClass) @@ -1668,7 +1935,7 @@ object Serializers { length, result, innerOffset, - If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, ArrayLength(dimensions.ref)), { + If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, arrayLength(dimensions.ref)), { Block( result2, innerComponentType, @@ -1687,7 +1954,7 @@ object Serializers { ) }) ) - }, Skip())(NoType), + }, Skip())(VoidType), result.ref ) } @@ -1738,11 +2005,11 @@ object Serializers { Block( outermostComponentType, i, - While(BinaryOp(BinaryOp.Int_!=, i.ref, ArrayLength(lengthsParam.ref)), { + While(BinaryOp(BinaryOp.Int_!=, i.ref, arrayLength(lengthsParam.ref)), { Block( Assign( outermostComponentType.ref, - GetClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)), + getClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)), MethodIdent(newInstanceSingleName), List(outermostComponentType.ref, IntLiteral(0)))(AnyType)) ), @@ -1771,7 +2038,7 @@ object Serializers { newInstanceRecMethod :: newMethods } - private def jsConstructorHack( + private def jsConstructorHackBelow11(ownerKind: ClassKind, jsMethodProps: List[JSMethodPropDef]): (Option[JSConstructorDef], List[JSMethodPropDef]) = { val jsConstructorBuilder = new OptionBuilder[JSConstructorDef] val jsMethodPropsBuilder = List.newBuilder[JSMethodPropDef] @@ -1785,8 +2052,9 @@ object Serializers { } bodyStats.span(!_.isInstanceOf[JSSuperConstructorCall]) match { - case (beforeSuper, (superCall: JSSuperConstructorCall) :: afterSuper) => + case (beforeSuper, (superCall: JSSuperConstructorCall) :: afterSuper0) => val newFlags = flags.withNamespace(MemberNamespace.Constructor) + val afterSuper = maybeHackJSConstructorDefAfterSuper(ownerKind, afterSuper0, superCall.pos) val newBody = JSConstructorBody(beforeSuper, superCall, afterSuper)(body.pos) val ctorDef = JSConstructorDef(newFlags, args, restParam, newBody)( methodDef.optimizerHints, Unversioned)(methodDef.pos) @@ -1808,13 +2076,95 @@ object Serializers { (jsConstructorBuilder.result(), jsMethodPropsBuilder.result()) } + /** Rewrites `scala.scalajs.runtime.AnonFunctionN`s from before 1.19. + * + * Before 1.19, these classes were defined as + * {{{ + * // final in source code + * class AnonFunctionN extends AbstractFunctionN { + * val f: any + * def this(f: any) = { + * this.f = f; + * super() + * } + * def apply(...args: any): any = f(...args) + * } + * }}} + * + * Starting with 1.19, they were rewritten to be used as SAM classes for + * `NewLambda` nodes. The new IR shape is + * {{{ + * // sealed abstract in source code + * class AnonFunctionN extends AbstractFunctionN { + * def this() = super() + * } + * }}} + * + * This function rewrites those classes to the new shape. + * + * The rewrite also applies to Scala 3's `AnonFunctionXXL`. + * + * Tests are in `sbt-plugin/src/sbt-test/linker/anonfunction-compat/`. + */ + private def anonFunctionClassDefHackBelow19(classDef: ClassDef): ClassDef = { + import classDef._ + + if (!HackNames.allAnonFunctionClasses.contains(className)) { + classDef + } else { + val newCtor: MethodDef = { + // Find the old constructor to get its position and version + val oldCtor = methods.find(_.methodName.isConstructor).getOrElse { + throw new InvalidIRException(classDef, + s"Did not find a constructor in ${className.nameString}") + } + implicit val pos = oldCtor.pos + + // constructor def () = this.superClass::() + MethodDef( + MemberFlags.empty.withNamespace(MemberNamespace.Constructor), + MethodIdent(NoArgConstructorName), + NoOriginalName, + Nil, + VoidType, + Some { + ApplyStatically( + ApplyFlags.empty.withConstructor(true), + This()(ClassType(className, nullable = false)), + superClass.get.name, + MethodIdent(NoArgConstructorName), + Nil + )(VoidType) + } + )(OptimizerHints.empty, oldCtor.version) + } + + ClassDef( + name, + originalName, + kind, + jsClassCaptures, + superClass, + interfaces, + jsSuperClass, + jsNativeLoadSpec, + fields = Nil, // throws away the `f` field + methods = List(newCtor), // throws away the old constructor and `apply` method + jsConstructor, + jsMethodProps, + jsNativeMembers, + topLevelExportDefs + )(OptimizerHints.empty)(pos) // throws away the `@inline` + } + } + private def readFieldDef()(implicit pos: Position): FieldDef = { val flags = MemberFlags.fromBits(readInt()) val name = readFieldIdentForEnclosingClass() val originalName = readOriginalName() val ftpe0 = readType() - val ftpe = if (hacks.use4 && ftpe0 == NothingType) { + val ftpe = if (hacks.useBelow(5) && ftpe0 == NothingType) { /* Note [Nothing FieldDef rewrite] * val field: nothing --> val field: null */ @@ -1848,7 +2198,7 @@ object Serializers { * rewrite it as a static initializers instead (``). */ val name0 = readMethodIdent() - if (hacks.use1 && + if (hacks.useBelow(2) && name0.name == ClassInitializerName && !ownerKind.isJSType) { MethodIdent(StaticInitializerName)(name0.pos) @@ -1863,21 +2213,21 @@ object Serializers { val body = readOptTree() val optimizerHints = OptimizerHints.fromBits(readInt()) - if (hacks.use0 && + if (hacks.useBelow(1) && flags.namespace == MemberNamespace.Public && owner == HackNames.SystemModule && name.name == HackNames.identityHashCodeName) { - /* #3976: 1.0 javalib relied on wrong linker dispatch. + /* #3976: Before 1.1, the javalib relied on wrong linker dispatch. * We simply replace it with a correct implementation. */ assert(args.size == 1) - val patchedBody = Some(IdentityHashCode(args(0).ref)) + val patchedBody = Some(UnaryOp(UnaryOp.IdentityHashCode, args(0).ref)) val patchedOptimizerHints = OptimizerHints.empty.withInline(true) MethodDef(flags, name, originalName, args, resultType, patchedBody)( patchedOptimizerHints, optHash) - } else if (hacks.use4 && + } else if (hacks.useBelow(5) && flags.namespace == MemberNamespace.Public && owner == ObjectClass && name.name == HackNames.cloneName) { @@ -1897,31 +2247,35 @@ object Serializers { val patchedBody = Some { If(IsInstanceOf(thisValue, cloneableClassType.toNonNullable), - Clone(AsInstanceOf(thisValue, cloneableClassType)), - Throw(New( - HackNames.CloneNotSupportedExceptionClass, - MethodIdent(NoArgConstructorName), - Nil)))(cloneableClassType) + UnaryOp(UnaryOp.Clone, + UnaryOp(UnaryOp.CheckNotNull, AsInstanceOf(thisValue, cloneableClassType))), + UnaryOp(UnaryOp.Throw, + New( + HackNames.CloneNotSupportedExceptionClass, + MethodIdent(NoArgConstructorName), + Nil)))(cloneableClassType) } val patchedOptimizerHints = OptimizerHints.empty.withInline(true) MethodDef(flags, name, originalName, args, resultType, patchedBody)( patchedOptimizerHints, optHash) } else { - val patchedBody = body.map(bodyHack5(_, isStat = resultType == NoType)) + val patchedBody = body.map(bodyHackBelow6(_, isStat = resultType == VoidType)) MethodDef(flags, name, originalName, args, resultType, patchedBody)( optimizerHints, optHash) } } - private def readJSConstructorDef()(implicit pos: Position): JSConstructorDef = { + private def readJSConstructorDef(ownerKind: ClassKind)( + implicit pos: Position): JSConstructorDef = { + val optHash = readOptHash() // read and discard the length val len = readInt() assert(len >= 0) /* JSConstructorDef was introduced in 1.11. Therefore, by - * construction, they never need the body hack of 1.5. + * construction, they never need the body hack below 1.6. */ val flags = MemberFlags.fromBits(readInt()) @@ -1929,12 +2283,25 @@ object Serializers { val bodyPos = readPosition() val beforeSuper = readTrees() val superCall = readTree().asInstanceOf[JSSuperConstructorCall] - val afterSuper = readTrees() + val afterSuper0 = readTrees() + val afterSuper = maybeHackJSConstructorDefAfterSuper(ownerKind, afterSuper0, superCall.pos) val body = JSConstructorBody(beforeSuper, superCall, afterSuper)(bodyPos) JSConstructorDef(flags, params, restParam, body)( OptimizerHints.fromBits(readInt()), optHash) } + private def maybeHackJSConstructorDefAfterSuper(ownerKind: ClassKind, + afterSuper0: List[Tree], superCallPos: Position): List[Tree] = { + if (hacks.useBelow(18) && ownerKind == ClassKind.JSModuleClass) { + afterSuper0 match { + case StoreModule() :: _ => afterSuper0 + case _ => StoreModule()(superCallPos) :: afterSuper0 + } + } else { + afterSuper0 + } + } + private def readJSMethodDef()(implicit pos: Position): JSMethodDef = { val optHash = readOptHash() // read and discard the length @@ -1942,16 +2309,16 @@ object Serializers { assert(len >= 0) val flags = MemberFlags.fromBits(readInt()) - val name = bodyHack5Expr(readTree()) + val name = bodyHackBelow6Expr(readTree()) val (params, restParam) = readParamDefsWithRest() - val body = bodyHack5Expr(readTree()) + val body = bodyHackBelow6Expr(readTree()) JSMethodDef(flags, name, params, restParam, body)( OptimizerHints.fromBits(readInt()), optHash) } private def readJSPropertyDef()(implicit pos: Position): JSPropertyDef = { val optHash: Version = { - if (hacks.use12) { + if (hacks.useBelow(13)) { Unversioned } else { val optHash = readOptHash() @@ -1963,11 +2330,11 @@ object Serializers { } val flags = MemberFlags.fromBits(readInt()) - val name = bodyHack5Expr(readTree()) - val getterBody = readOptTree().map(bodyHack5Expr(_)) + val name = bodyHackBelow6Expr(readTree()) + val getterBody = readOptTree().map(bodyHackBelow6Expr(_)) val setterArgAndBody = { if (readBoolean()) - Some((readParamDef(), bodyHack5Expr(readTree()))) + Some((readParamDef(), bodyHackBelow6Expr(readTree()))) else None } @@ -1981,41 +2348,70 @@ object Serializers { JSNativeMemberDef(flags, name, jsNativeLoadSpec) } - private def bodyHack5(body: Tree, isStat: Boolean): Tree = { - if (!hacks.use5) { - body - } else { - /* #4442 and #4601: Patch Labeled, If, Match and TryCatch nodes in - * statement position to have type NoType. These 4 nodes are the - * control structures whose result type is explicitly specified (and - * not derived from their children like Block or TryFinally, or - * constant like While). - */ - new Transformers.Transformer { - override def transform(tree: Tree, isStat: Boolean): Tree = { - val newTree = super.transform(tree, isStat) - if (isStat && newTree.tpe != NoType) { - newTree match { - case Labeled(label, _, body) => - Labeled(label, NoType, body)(newTree.pos) - case If(cond, thenp, elsep) => - If(cond, thenp, elsep)(NoType)(newTree.pos) - case Match(selector, cases, default) => - Match(selector, cases, default)(NoType)(newTree.pos) - case TryCatch(block, errVar, errVarOriginalName, handler) => - TryCatch(block, errVar, errVarOriginalName, handler)(NoType)(newTree.pos) - case _ => - newTree - } - } else { - newTree - } - } - }.transform(body, isStat) + /* #4442 and #4601: Patch Labeled, If, Match and TryCatch nodes in + * statement position to have type VoidType. These 4 nodes are the + * control structures whose result type is explicitly specified (and + * not derived from their children like Block or TryFinally, or + * constant like While). + */ + private object BodyHackBelow6Transformer extends Transformers.Transformer { + def transformStat(tree: Tree): Tree = { + implicit val pos = tree.pos + + tree match { + // Nodes that we actually need to alter + case Labeled(label, _, body) => + Labeled(label, VoidType, transformStat(body)) + case If(cond, thenp, elsep) => + If(transform(cond), transformStat(thenp), transformStat(elsep))(VoidType) + case Match(selector, cases, default) => + Match(transform(selector), cases.map(c => (c._1, transformStat(c._2))), + transformStat(default))(VoidType) + case TryCatch(block, errVar, errVarOriginalName, handler) => + TryCatch(transformStat(block), errVar, errVarOriginalName, + transformStat(handler))(VoidType) + + // Nodes for which we need to forward the statement position + case Block(stats) => + Block(stats.map(transformStat(_))) + case TryFinally(block, finalizer) => + Block(transformStat(block), transformStat(finalizer)) + + // For everything else, delegate to transform + case _ => + transform(tree) + } } + + override def transform(tree: Tree): Tree = { + implicit val pos = tree.pos + + tree match { + // Nodes that force a statement position for some of their parts + case Block(stats) => + Block(stats.init.map(transformStat(_)), transform(stats.last)) + case While(cond, body) => + While(transform(cond), transformStat(body)) + case ForIn(obj, keyVar, keyVarOriginalName, body) => + ForIn(transform(obj), keyVar, keyVarOriginalName, transformStat(body)) + case TryFinally(block, finalizer) => + TryFinally(transform(block), transformStat(finalizer)) + + case _ => + super.transform(tree) + } + } + + def transform(tree: Tree, isStat: Boolean): Tree = + if (isStat) transformStat(tree) + else transform(tree) } - private def bodyHack5Expr(body: Tree): Tree = bodyHack5(body, isStat = false) + private def bodyHackBelow6(body: Tree, isStat: Boolean): Tree = + if (!hacks.useBelow(6)) body + else BodyHackBelow6Transformer.transform(body, isStat) + + private def bodyHackBelow6Expr(body: Tree): Tree = bodyHackBelow6(body, isStat = false) def readTopLevelExportDef(): TopLevelExportDef = { implicit val pos = readPosition() @@ -2029,7 +2425,7 @@ object Serializers { } def readModuleID(): String = - if (hacks.use2) DefaultModuleID + if (hacks.useBelow(3)) DefaultModuleID else readString() (tag: @switch) match { @@ -2050,11 +2446,6 @@ object Serializers { LocalIdent(readLocalName()) } - def readLabelIdent(): LabelIdent = { - implicit val pos = readPosition() - LabelIdent(readLabelName()) - } - def readFieldIdent(): FieldIdent = { // For historical reasons, the className comes *before* the position val className = readClassName() @@ -2099,7 +2490,7 @@ object Serializers { val ptpe = readType() val mutable = readBoolean() - if (hacks.use4) { + if (hacks.useBelow(5)) { val rest = readBoolean() assert(!rest, "Illegal rest parameter") } @@ -2111,7 +2502,7 @@ object Serializers { List.fill(readInt())(readParamDef()) def readParamDefsWithRest(): (List[ParamDef], Option[ParamDef]) = { - if (hacks.use4) { + if (hacks.useBelow(5)) { val (params, isRest) = List.fill(readInt()) { implicit val pos = readPosition() (ParamDef(readLocalIdent(), readOriginalName(), readType(), readBoolean()), readBoolean()) @@ -2151,7 +2542,7 @@ object Serializers { case TagDoubleType => DoubleType case TagStringType => StringType case TagNullType => NullType - case TagNoType => NoType + case TagVoidType => VoidType case TagClassType => ClassType(readClassName(), nullable = true) case TagArrayType => ArrayType(readArrayTypeRef(), nullable = true) @@ -2159,6 +2550,11 @@ object Serializers { case TagNonNullClassType => ClassType(readClassName(), nullable = false) case TagNonNullArrayType => ArrayType(readArrayTypeRef(), nullable = false) + case TagClosureType | TagNonNullClosureType => + val paramTypes = readTypes() + val resultType = readType() + ClosureType(paramTypes, resultType, nullable = tag == TagClosureType) + case TagRecordType => RecordType(List.fill(readInt()) { val name = readSimpleFieldName() @@ -2170,6 +2566,9 @@ object Serializers { } } + def readTypes(): List[Type] = + List.fill(readInt())(readType()) + def readTypeRef(): TypeRef = { readByte() match { case TagVoidRef => VoidRef @@ -2194,6 +2593,14 @@ object Serializers { def readApplyFlags(): ApplyFlags = ApplyFlags.fromBits(readInt()) + def readClosureFlags(): ClosureFlags = { + /* Before 1.19, the `flags` were a single `Boolean` for the `arrow` flag. + * The bit pattern of `flags` was crafted so that it matches the old + * boolean encoding for common values. + */ + ClosureFlags.fromBits(readByte()) + } + def readPosition(): Position = { import PositionFormat._ @@ -2283,6 +2690,12 @@ object Serializers { } private def readLabelName(): LabelName = { + /* Before 1.18, `LabelName`s were always wrapped in `LabelIdent`s, whose + * encoding was a `Position` followed by the actual `LabelName`. + */ + if (hacks.useBelow(18)) + readPosition() // intentional discard + val i = readInt() val existing = labelNames(i) if (existing ne null) { @@ -2330,6 +2743,9 @@ object Serializers { } } + private def readClassNames(): List[ClassName] = + List.fill(readInt())(readClassName()) + private def readMethodName(): MethodName = methodNames(readInt()) @@ -2396,43 +2812,25 @@ object Serializers { } } - /** Hacks for backwards compatible deserializing. */ - private final class Hacks(sourceVersion: String) { - val use0: Boolean = sourceVersion == "1.0" - - val use1: Boolean = use0 || sourceVersion == "1.1" - - val use2: Boolean = use1 || sourceVersion == "1.2" - - private val use3: Boolean = use2 || sourceVersion == "1.3" - - val use4: Boolean = use3 || sourceVersion == "1.4" - - val use5: Boolean = use4 || sourceVersion == "1.5" - - private val use6: Boolean = use5 || sourceVersion == "1.6" - - private val use7: Boolean = use6 || sourceVersion == "1.7" - - val use8: Boolean = use7 || sourceVersion == "1.8" - - assert(sourceVersion != "1.9", "source version 1.9 does not exist") - assert(sourceVersion != "1.10", "source version 1.10 does not exist") - - private val use11: Boolean = use8 || sourceVersion == "1.11" - - val use12: Boolean = use11 || sourceVersion == "1.12" - - val use13: Boolean = use12 || sourceVersion == "1.13" - - assert(sourceVersion != "1.14", "source version 1.14 does not exist") - assert(sourceVersion != "1.15", "source version 1.15 does not exist") + /** Hacks for backwards compatible deserializing. + * + * `private[ir]` for testing purposes only. + */ + private[ir] final class Hacks(sourceVersion: String) { + private val fromVersion = sourceVersion match { + case CompatibleStableIRVersionRegex(minorDigits) => minorDigits.toInt + case _ => Int.MaxValue // never use any hack + } - val use16: Boolean = use13 || sourceVersion == "1.16" + /** Should we use the hacks to migrate from an IR version below `targetVersion`? */ + def useBelow(targetVersion: Int): Boolean = + fromVersion < targetVersion } /** Names needed for hacks. */ private object HackNames { + val AnonFunctionXXLClass = + ClassName("scala.scalajs.runtime.AnonFunctionXXL") // from the Scala 3 library val CloneNotSupportedExceptionClass = ClassName("java.lang.CloneNotSupportedException") val SystemModule: ClassName = @@ -2442,18 +2840,54 @@ object Serializers { val ReflectArrayModClass = ClassName("java.lang.reflect.Array$") + val ObjectArrayType = ArrayType(ArrayTypeRef(ObjectRef, 1), nullable = true) + + private val applySimpleName = SimpleMethodName("apply") + val cloneName: MethodName = - MethodName("clone", Nil, ClassRef(ObjectClass)) + MethodName("clone", Nil, ObjectRef) val identityHashCodeName: MethodName = - MethodName("identityHashCode", List(ClassRef(ObjectClass)), IntRef) + MethodName("identityHashCode", List(ObjectRef), IntRef) val newInstanceSingleName: MethodName = - MethodName("newInstance", List(ClassRef(ClassClass), IntRef), ClassRef(ObjectClass)) + MethodName("newInstance", List(ClassRef(ClassClass), IntRef), ObjectRef) val newInstanceMultiName: MethodName = - MethodName("newInstance", List(ClassRef(ClassClass), ArrayTypeRef(IntRef, 1)), ClassRef(ObjectClass)) + MethodName("newInstance", List(ClassRef(ClassClass), ArrayTypeRef(IntRef, 1)), ObjectRef) + + private val anonFunctionArities: Map[ClassName, Int] = + (0 to 22).map(arity => ClassName(s"scala.scalajs.runtime.AnonFunction$arity") -> arity).toMap + val allAnonFunctionClasses: Set[ClassName] = + anonFunctionArities.keySet + AnonFunctionXXLClass + + object AnonFunctionClass { + def unapply(cls: ClassName): Option[Int] = + anonFunctionArities.get(cls) + } + + lazy val anonFunctionDescriptors: IndexedSeq[NewLambda.Descriptor] = { + anonFunctionArities.toIndexedSeq.sortBy(_._2).map { case (className, arity) => + NewLambda.Descriptor( + superClass = className, + interfaces = Nil, + methodName = MethodName(applySimpleName, List.fill(arity)(ObjectRef), ObjectRef), + paramTypes = List.fill(arity)(AnyType), + resultType = AnyType + ) + } + } + + lazy val anonFunctionXXLDescriptor: NewLambda.Descriptor = { + NewLambda.Descriptor( + superClass = AnonFunctionXXLClass, + interfaces = Nil, + methodName = MethodName(applySimpleName, List(ObjectArrayType.arrayTypeRef), ObjectRef), + paramTypes = List(ObjectArrayType), + resultType = AnyType + ) + } } private class OptionBuilder[T] { - private[this] var value: Option[T] = None + private var value: Option[T] = None def +=(x: T): Unit = { require(value.isEmpty) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala b/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala index 3c3162245b..dc2862b7ec 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala @@ -127,6 +127,17 @@ private[ir] object Tags { final val TagWrapAsThrowable = TagJSNewTarget + 1 final val TagUnwrapFromThrowable = TagWrapAsThrowable + 1 + // New in 1.18 + final val TagLinkTimeProperty = TagUnwrapFromThrowable + 1 + + // New in 1.19 + final val TagApplyTypedClosure = TagLinkTimeProperty + 1 + final val TagNewLambda = TagApplyTypedClosure + 1 + final val TagJSAwait = TagNewLambda + 1 + + // New in 1.20 + final val TagLinkTimeIf = TagJSAwait + 1 + // Tags for member defs final val TagFieldDef = 1 @@ -168,14 +179,22 @@ private[ir] object Tags { final val TagClassType = TagNullType + 1 final val TagArrayType = TagClassType + 1 final val TagRecordType = TagArrayType + 1 - final val TagNoType = TagRecordType + 1 + final val TagVoidType = TagRecordType + 1 + + @deprecated("Use TagVoidType instead", since = "1.18.0") + final val TagNoType = TagVoidType // New in 1.17 - final val TagAnyNotNullType = TagNoType + 1 + final val TagAnyNotNullType = TagVoidType + 1 final val TagNonNullClassType = TagAnyNotNullType + 1 final val TagNonNullArrayType = TagNonNullClassType + 1 + // New in 1.19 + + final val TagClosureType = TagNonNullArrayType + 1 + final val TagNonNullClosureType = TagClosureType + 1 + // Tags for TypeRefs final val TagVoidRef = 1 @@ -192,6 +211,10 @@ private[ir] object Tags { final val TagClassRef = TagNothingRef + 1 final val TagArrayTypeRef = TagClassRef + 1 + // New in 1.19 + + final val TagTransientTypeRefHashingOnly = TagArrayTypeRef + 1 + // Tags for JS native loading specs final val TagJSNativeLoadSpecNone = 0 diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala index 09b82b1757..e95a154e1c 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -19,211 +19,195 @@ import Version.Unversioned object Transformers { abstract class Transformer { - final def transformStats(trees: List[Tree]): List[Tree] = - trees.map(transformStat(_)) - - final def transformStat(tree: Tree): Tree = - transform(tree, isStat = true) - - final def transformExpr(tree: Tree): Tree = - transform(tree, isStat = false) - - def transformExprOrJSSpread(tree: TreeOrJSSpread): TreeOrJSSpread = { + final def transformTreeOrJSSpread(tree: TreeOrJSSpread): TreeOrJSSpread = { implicit val pos = tree.pos tree match { - case JSSpread(items) => JSSpread(transformExpr(items)) - case tree: Tree => transformExpr(tree) + case JSSpread(items) => JSSpread(transform(items)) + case tree: Tree => transform(tree) } } - def transform(tree: Tree, isStat: Boolean): Tree = { + final def transformTrees(trees: List[Tree]): List[Tree] = + trees.map(transform(_)) + + final def transformTreeOpt(treeOpt: Option[Tree]): Option[Tree] = + treeOpt.map(transform(_)) + + def transform(tree: Tree): Tree = { implicit val pos = tree.pos tree match { // Definitions case VarDef(ident, originalName, vtpe, mutable, rhs) => - VarDef(ident, originalName, vtpe, mutable, transformExpr(rhs)) + VarDef(ident, originalName, vtpe, mutable, transform(rhs)) // Control flow constructs case Block(stats) => - Block(stats.init.map(transformStat) :+ transform(stats.last, isStat)) + Block(transformTrees(stats)) case Labeled(label, tpe, body) => - Labeled(label, tpe, transform(body, isStat)) + Labeled(label, tpe, transform(body)) case Assign(lhs, rhs) => - Assign(transformExpr(lhs).asInstanceOf[AssignLhs], transformExpr(rhs)) + Assign(transform(lhs).asInstanceOf[AssignLhs], transform(rhs)) case Return(expr, label) => - Return(transformExpr(expr), label) + Return(transform(expr), label) case If(cond, thenp, elsep) => - If(transformExpr(cond), transform(thenp, isStat), - transform(elsep, isStat))(tree.tpe) + If(transform(cond), transform(thenp), transform(elsep))(tree.tpe) + + case LinkTimeIf(cond, thenp, elsep) => + LinkTimeIf(transform(cond), transform(thenp), transform(elsep))(tree.tpe) case While(cond, body) => - While(transformExpr(cond), transformStat(body)) + While(transform(cond), transform(body)) case ForIn(obj, keyVar, keyVarOriginalName, body) => - ForIn(transformExpr(obj), keyVar, keyVarOriginalName, - transformStat(body)) + ForIn(transform(obj), keyVar, keyVarOriginalName, transform(body)) case TryCatch(block, errVar, errVarOriginalName, handler) => - TryCatch(transform(block, isStat), errVar, errVarOriginalName, - transform(handler, isStat))(tree.tpe) + TryCatch(transform(block), errVar, errVarOriginalName, + transform(handler))(tree.tpe) case TryFinally(block, finalizer) => - TryFinally(transform(block, isStat), transformStat(finalizer)) - - case Throw(expr) => - Throw(transformExpr(expr)) + TryFinally(transform(block), transform(finalizer)) case Match(selector, cases, default) => - Match(transformExpr(selector), - cases map (c => (c._1, transform(c._2, isStat))), - transform(default, isStat))(tree.tpe) + Match(transform(selector), cases.map(c => (c._1, transform(c._2))), + transform(default))(tree.tpe) + + case JSAwait(arg) => + JSAwait(transform(arg)) // Scala expressions case New(className, ctor, args) => - New(className, ctor, args map transformExpr) + New(className, ctor, transformTrees(args)) case Select(qualifier, field) => - Select(transformExpr(qualifier), field)(tree.tpe) + Select(transform(qualifier), field)(tree.tpe) case Apply(flags, receiver, method, args) => - Apply(flags, transformExpr(receiver), method, - args map transformExpr)(tree.tpe) + Apply(flags, transform(receiver), method, + transformTrees(args))(tree.tpe) case ApplyStatically(flags, receiver, className, method, args) => - ApplyStatically(flags, transformExpr(receiver), className, method, - args map transformExpr)(tree.tpe) + ApplyStatically(flags, transform(receiver), className, method, + transformTrees(args))(tree.tpe) case ApplyStatic(flags, className, method, args) => - ApplyStatic(flags, className, method, args map transformExpr)(tree.tpe) + ApplyStatic(flags, className, method, transformTrees(args))(tree.tpe) case ApplyDynamicImport(flags, className, method, args) => - ApplyDynamicImport(flags, className, method, args.map(transformExpr)) + ApplyDynamicImport(flags, className, method, transformTrees(args)) + + case ApplyTypedClosure(flags, fun, args) => + ApplyTypedClosure(flags, transform(fun), transformTrees(args)) + + case NewLambda(descriptor, fun) => + NewLambda(descriptor, transform(fun))(tree.tpe) case UnaryOp(op, lhs) => - UnaryOp(op, transformExpr(lhs)) + UnaryOp(op, transform(lhs)) case BinaryOp(op, lhs, rhs) => - BinaryOp(op, transformExpr(lhs), transformExpr(rhs)) + BinaryOp(op, transform(lhs), transform(rhs)) case NewArray(tpe, length) => - NewArray(tpe, transformExpr(length)) + NewArray(tpe, transform(length)) case ArrayValue(tpe, elems) => - ArrayValue(tpe, elems map transformExpr) - - case ArrayLength(array) => - ArrayLength(transformExpr(array)) + ArrayValue(tpe, transformTrees(elems)) case ArraySelect(array, index) => - ArraySelect(transformExpr(array), transformExpr(index))(tree.tpe) + ArraySelect(transform(array), transform(index))(tree.tpe) case RecordValue(tpe, elems) => - RecordValue(tpe, elems map transformExpr) + RecordValue(tpe, transformTrees(elems)) case RecordSelect(record, field) => - RecordSelect(transformExpr(record), field)(tree.tpe) + RecordSelect(transform(record), field)(tree.tpe) case IsInstanceOf(expr, testType) => - IsInstanceOf(transformExpr(expr), testType) + IsInstanceOf(transform(expr), testType) case AsInstanceOf(expr, tpe) => - AsInstanceOf(transformExpr(expr), tpe) - - case GetClass(expr) => - GetClass(transformExpr(expr)) - - case Clone(expr) => - Clone(transformExpr(expr)) - - case IdentityHashCode(expr) => - IdentityHashCode(transformExpr(expr)) - - case WrapAsThrowable(expr) => - WrapAsThrowable(transformExpr(expr)) - - case UnwrapFromThrowable(expr) => - UnwrapFromThrowable(transformExpr(expr)) + AsInstanceOf(transform(expr), tpe) // JavaScript expressions case JSNew(ctor, args) => - JSNew(transformExpr(ctor), args.map(transformExprOrJSSpread)) + JSNew(transform(ctor), args.map(transformTreeOrJSSpread)) case JSPrivateSelect(qualifier, field) => - JSPrivateSelect(transformExpr(qualifier), field) + JSPrivateSelect(transform(qualifier), field) case JSSelect(qualifier, item) => - JSSelect(transformExpr(qualifier), transformExpr(item)) + JSSelect(transform(qualifier), transform(item)) case JSFunctionApply(fun, args) => - JSFunctionApply(transformExpr(fun), args.map(transformExprOrJSSpread)) + JSFunctionApply(transform(fun), args.map(transformTreeOrJSSpread)) case JSMethodApply(receiver, method, args) => - JSMethodApply(transformExpr(receiver), transformExpr(method), - args.map(transformExprOrJSSpread)) + JSMethodApply(transform(receiver), transform(method), + args.map(transformTreeOrJSSpread)) case JSSuperSelect(superClass, qualifier, item) => - JSSuperSelect(superClass, transformExpr(qualifier), - transformExpr(item)) + JSSuperSelect(superClass, transform(qualifier), transform(item)) case JSSuperMethodCall(superClass, receiver, method, args) => - JSSuperMethodCall(superClass, transformExpr(receiver), - transformExpr(method), args.map(transformExprOrJSSpread)) + JSSuperMethodCall(superClass, transform(receiver), + transform(method), args.map(transformTreeOrJSSpread)) case JSSuperConstructorCall(args) => - JSSuperConstructorCall(args.map(transformExprOrJSSpread)) + JSSuperConstructorCall(args.map(transformTreeOrJSSpread)) case JSImportCall(arg) => - JSImportCall(transformExpr(arg)) + JSImportCall(transform(arg)) case JSDelete(qualifier, item) => - JSDelete(transformExpr(qualifier), transformExpr(item)) + JSDelete(transform(qualifier), transform(item)) case JSUnaryOp(op, lhs) => - JSUnaryOp(op, transformExpr(lhs)) + JSUnaryOp(op, transform(lhs)) case JSBinaryOp(op, lhs, rhs) => - JSBinaryOp(op, transformExpr(lhs), transformExpr(rhs)) + JSBinaryOp(op, transform(lhs), transform(rhs)) case JSArrayConstr(items) => - JSArrayConstr(items.map(transformExprOrJSSpread)) + JSArrayConstr(items.map(transformTreeOrJSSpread)) case JSObjectConstr(fields) => JSObjectConstr(fields.map { field => - (transformExpr(field._1), transformExpr(field._2)) + (transform(field._1), transform(field._2)) }) case JSTypeOfGlobalRef(globalRef) => - JSTypeOfGlobalRef(transformExpr(globalRef).asInstanceOf[JSGlobalRef]) + JSTypeOfGlobalRef(transform(globalRef).asInstanceOf[JSGlobalRef]) // Atomic expressions - case Closure(arrow, captureParams, params, restParam, body, captureValues) => - Closure(arrow, captureParams, params, restParam, transformExpr(body), - captureValues.map(transformExpr)) + case Closure(flags, captureParams, params, restParam, resultType, body, captureValues) => + Closure(flags, captureParams, params, restParam, resultType, + transform(body), transformTrees(captureValues)) case CreateJSClass(className, captureValues) => - CreateJSClass(className, captureValues.map(transformExpr)) + CreateJSClass(className, transformTrees(captureValues)) // Transients case Transient(value) => - value.transform(this, isStat) + value.transform(this) // Trees that need not be transformed case _:Skip | _:Debugger | _:LoadModule | _:StoreModule | _:SelectStatic | _:SelectJSNativeMember | _:LoadJSConstructor | - _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo | - _:Literal | _:VarRef | _:This | _:JSGlobalRef => + _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | + _:Literal | _:VarRef | _:JSGlobalRef | _:LinkTimeProperty => tree } } @@ -233,7 +217,7 @@ object Transformers { def transformClassDef(tree: ClassDef): ClassDef = { import tree._ ClassDef(name, originalName, kind, jsClassCaptures, superClass, - interfaces, jsSuperClass.map(transformExpr), jsNativeLoadSpec, + interfaces, transformTreeOpt(jsSuperClass), jsNativeLoadSpec, fields.map(transformAnyFieldDef(_)), methods.map(transformMethodDef), jsConstructor.map(transformJSConstructorDef), jsMethodProps.map(transformJSMethodPropDef), jsNativeMembers, @@ -246,7 +230,7 @@ object Transformers { def transformMethodDef(methodDef: MethodDef): MethodDef = { val MethodDef(flags, name, originalName, args, resultType, body) = methodDef - val newBody = body.map(transform(_, isStat = resultType == NoType)) + val newBody = transformTreeOpt(body) MethodDef(flags, name, originalName, args, resultType, newBody)( methodDef.optimizerHints, Unversioned)(methodDef.pos) } @@ -262,32 +246,35 @@ object Transformers { case jsMethodDef: JSMethodDef => transformJSMethodDef(jsMethodDef) - case JSPropertyDef(flags, name, getterBody, setterArgAndBody) => - JSPropertyDef( - flags, - transformExpr(name), - getterBody.map(transformStat), - setterArgAndBody map { case (arg, body) => - (arg, transformStat(body)) - })(Unversioned)(jsMethodPropDef.pos) + case jsPropertyDef: JSPropertyDef => + transformJSPropertyDef(jsPropertyDef) } } def transformJSMethodDef(jsMethodDef: JSMethodDef): JSMethodDef = { val JSMethodDef(flags, name, args, restParam, body) = jsMethodDef - JSMethodDef(flags, transformExpr(name), args, restParam, transformExpr(body))( + JSMethodDef(flags, transform(name), args, restParam, transform(body))( jsMethodDef.optimizerHints, Unversioned)(jsMethodDef.pos) } + def transformJSPropertyDef(jsPropertyDef: JSPropertyDef): JSPropertyDef = { + val JSPropertyDef(flags, name, getterBody, setterArgAndBody) = jsPropertyDef + JSPropertyDef( + flags, + transform(name), + transformTreeOpt(getterBody), + setterArgAndBody.map { case (arg, body) => + (arg, transform(body)) + } + )(Unversioned)(jsPropertyDef.pos) + } + def transformJSConstructorBody(body: JSConstructorBody): JSConstructorBody = { implicit val pos = body.pos - val newBeforeSuper = body.beforeSuper.map(transformStat(_)) - val newSuperCall = transformStat(body.superCall).asInstanceOf[JSSuperConstructorCall] - val newAfterSuper = body.afterSuper match { - case stats :+ expr => stats.map(transformStat(_)) :+ transformExpr(expr) - case empty => empty // cannot use Nil here because the compiler does not know that it is exhaustive - } + val newBeforeSuper = transformTrees(body.beforeSuper) + val newSuperCall = transform(body.superCall).asInstanceOf[JSSuperConstructorCall] + val newAfterSuper = transformTrees(body.afterSuper) JSConstructorBody(newBeforeSuper, newSuperCall, newAfterSuper) } @@ -308,4 +295,19 @@ object Transformers { } } + /** Transformer that only transforms in the local scope. + * + * In practice, this means stopping at `Closure` boundaries: their + * `captureValues` are transformed, but not their other members. + */ + abstract class LocalScopeTransformer extends Transformer { + override def transform(tree: Tree): Tree = tree match { + case Closure(flags, captureParams, params, restParam, resultType, body, captureValues) => + Closure(flags, captureParams, params, restParam, resultType, body, + transformTrees(captureValues))(tree.pos) + case _ => + super.transform(tree) + } + } + } diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala index 2f68fac81f..15c9da9093 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -48,6 +48,11 @@ object Traversers { traverse(thenp) traverse(elsep) + case LinkTimeIf(cond, thenp, elsep) => + traverse(cond) + traverse(thenp) + traverse(elsep) + case While(cond, body) => traverse(cond) traverse(body) @@ -64,14 +69,14 @@ object Traversers { traverse(block) traverse(finalizer) - case Throw(expr) => - traverse(expr) - case Match(selector, cases, default) => traverse(selector) cases foreach (c => (c._1 map traverse, traverse(c._2))) traverse(default) + case JSAwait(arg) => + traverse(arg) + // Scala expressions case New(_, _, args) => @@ -94,6 +99,13 @@ object Traversers { case ApplyDynamicImport(_, _, _, args) => args.foreach(traverse) + case ApplyTypedClosure(_, fun, args) => + traverse(fun) + args.foreach(traverse) + + case NewLambda(_, fun) => + traverse(fun) + case UnaryOp(op, lhs) => traverse(lhs) @@ -107,9 +119,6 @@ object Traversers { case ArrayValue(tpe, elems) => elems foreach traverse - case ArrayLength(array) => - traverse(array) - case ArraySelect(array, index) => traverse(array) traverse(index) @@ -126,21 +135,6 @@ object Traversers { case AsInstanceOf(expr, _) => traverse(expr) - case GetClass(expr) => - traverse(expr) - - case Clone(expr) => - traverse(expr) - - case IdentityHashCode(expr) => - traverse(expr) - - case WrapAsThrowable(expr) => - traverse(expr) - - case UnwrapFromThrowable(expr) => - traverse(expr) - // JavaScript expressions case JSNew(ctor, args) => @@ -205,7 +199,7 @@ object Traversers { // Atomic expressions - case Closure(arrow, captureParams, params, restParam, body, captureValues) => + case Closure(flags, captureParams, params, restParam, resultType, body, captureValues) => traverse(body) captureValues.foreach(traverse) @@ -221,8 +215,8 @@ object Traversers { case _:Skip | _:Debugger | _:LoadModule | _:StoreModule | _:SelectStatic | _:SelectJSNativeMember | _:LoadJSConstructor | - _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo | - _:Literal | _:VarRef | _:This | _:JSGlobalRef => + _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | + _:Literal | _:VarRef | _:JSGlobalRef | _:LinkTimeProperty => } def traverseClassDef(tree: ClassDef): Unit = { @@ -266,4 +260,18 @@ object Traversers { } } + /** Traverser that only traverses the local scope. + * + * In practice, this means stopping at `Closure` boundaries: their + * `captureValues` are traversed, but not their other members. + */ + abstract class LocalScopeTraverser extends Traverser { + override def traverse(tree: Tree): Unit = tree match { + case Closure(_, _, _, _, _, _, captureValues) => + captureValues.foreach(traverse(_)) + case _ => + super.traverse(tree) + } + } + } diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index 6cc39f24a8..d0cc772980 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -18,11 +18,14 @@ import Names._ import OriginalName.NoOriginalName import Position.NoPosition import Types._ +import WellKnownNames._ object Trees { - /* The case classes for IR Nodes are sealed instead of final because making - * them final triggers bugs with Scala 2.12.{1-4}, in combination - * with their `implicit val pos`. + /* The case classes for IR Nodes are sealed instead of final for historical + * reasons. Making them final used to trigger bugs with Scala 2.12.{1-4}, in + * combination with their `implicit val pos`. + * TODO Now that we dropped support for Scala 2.12.5 and below, we should + * revisit this. */ /** Base class for all nodes in the IR. @@ -56,9 +59,6 @@ object Trees { sealed case class LocalIdent(name: LocalName)(implicit val pos: Position) extends IRNode - sealed case class LabelIdent(name: LabelName)(implicit val pos: Position) - extends IRNode - sealed case class SimpleFieldIdent(name: SimpleFieldName)(implicit val pos: Position) extends IRNode @@ -102,21 +102,21 @@ object Trees { sealed case class VarDef(name: LocalIdent, originalName: OriginalName, vtpe: Type, mutable: Boolean, rhs: Tree)( implicit val pos: Position) extends Tree { - val tpe = NoType // cannot be in expression position + val tpe = VoidType - def ref(implicit pos: Position): VarRef = VarRef(name)(vtpe) + def ref(implicit pos: Position): VarRef = VarRef(name.name)(vtpe) } sealed case class ParamDef(name: LocalIdent, originalName: OriginalName, ptpe: Type, mutable: Boolean)( implicit val pos: Position) extends IRNode { - def ref(implicit pos: Position): VarRef = VarRef(name)(ptpe) + def ref(implicit pos: Position): VarRef = VarRef(name.name)(ptpe) } // Control flow constructs sealed case class Skip()(implicit val pos: Position) extends Tree { - val tpe = NoType // cannot be in expression position + val tpe = VoidType } sealed class Block private (val stats: List[Tree])( @@ -150,17 +150,17 @@ object Trees { def unapply(block: Block): Some[List[Tree]] = Some(block.stats) } - sealed case class Labeled(label: LabelIdent, tpe: Type, body: Tree)( + sealed case class Labeled(label: LabelName, tpe: Type, body: Tree)( implicit val pos: Position) extends Tree sealed trait AssignLhs extends Tree sealed case class Assign(lhs: AssignLhs, rhs: Tree)( implicit val pos: Position) extends Tree { - val tpe = NoType // cannot be in expression position + val tpe = VoidType } - sealed case class Return(expr: Tree, label: LabelIdent)( + sealed case class Return(expr: Tree, label: LabelName)( implicit val pos: Position) extends Tree { val tpe = NothingType } @@ -168,19 +168,50 @@ object Trees { sealed case class If(cond: Tree, thenp: Tree, elsep: Tree)(val tpe: Type)( implicit val pos: Position) extends Tree + /** Link-time `if` expression. + * + * The `cond` must be a well-typed link-time tree of type `boolean`. + * + * A link-time tree is a `Tree` matching the following sub-grammar: + * + * {{{ + * link-time-tree ::= + * BooleanLiteral + * | IntLiteral + * | StringLiteral + * | LinkTimeProperty + * | UnaryOp(link-time-unary-op, link-time-tree) + * | BinaryOp(link-time-binary-op, link-time-tree, link-time-tree) + * | LinkTimeIf(link-time-tree, link-time-tree, link-time-tree) + * + * link-time-unary-op ::= + * Boolean_! + * + * link-time-binary-op ::= + * Boolean_== | Boolean_!= | Boolean_| | Boolean_& + * | Int_== | Int_!= | Int_< | Int_<= | Int_> | Int_>= + * }}} + * + * Note: nested `LinkTimeIf` nodes in the `cond` are used to encode + * short-circuiting boolean `&&` and `||`, just like we do with regular + * `If` nodes. + */ + sealed case class LinkTimeIf(cond: Tree, thenp: Tree, elsep: Tree)( + val tpe: Type)(implicit val pos: Position) + extends Tree + sealed case class While(cond: Tree, body: Tree)( implicit val pos: Position) extends Tree { - // cannot be in expression position, unless it is infinite val tpe = cond match { case BooleanLiteral(true) => NothingType - case _ => NoType + case _ => VoidType } } sealed case class ForIn(obj: Tree, keyVar: LocalIdent, keyVarOriginalName: OriginalName, body: Tree)( implicit val pos: Position) extends Tree { - val tpe = NoType + val tpe = VoidType } sealed case class TryCatch(block: Tree, errVar: LocalIdent, @@ -192,10 +223,6 @@ object Trees { val tpe = block.tpe } - sealed case class Throw(expr: Tree)(implicit val pos: Position) extends Tree { - val tpe = NothingType - } - /** A break-free switch (without fallthrough behavior). * * Unlike a JavaScript switch, it can be used in expression position. @@ -223,8 +250,24 @@ object Trees { sealed case class Match(selector: Tree, cases: List[(List[MatchableLiteral], Tree)], default: Tree)(val tpe: Type)(implicit val pos: Position) extends Tree + /** `await arg`. + * + * This is directly equivalent to a JavaScript `await` expression. + * + * If used directly within a [[Closure]] node with the `async` flag, this + * node is always valid. However, when used anywhere else, it is an "orphan" + * await. Orphan awaits only link when targeting WebAssembly. + * + * This is not a `UnaryOp` because of the above strict scoping rule. For + * example, unless it is orphan to begin with, it is not safe to pull this + * node out of or into an intervening closure, contrary to `UnaryOp`s. + */ + sealed case class JSAwait(arg: Tree)(implicit val pos: Position) extends Tree { + val tpe = AnyType + } + sealed case class Debugger()(implicit val pos: Position) extends Tree { - val tpe = NoType // cannot be in expression position + val tpe = VoidType } // Scala expressions @@ -246,7 +289,7 @@ object Trees { } sealed case class StoreModule()(implicit val pos: Position) extends Tree { - val tpe = NoType // cannot be in expression position + val tpe = VoidType } sealed case class Select(qualifier: Tree, field: FieldIdent)(val tpe: Type)( @@ -283,13 +326,129 @@ object Trees { val tpe = AnyType } + /** Apply a typed closure + * + * The given `fun` must have a closure type. + * + * The arguments' types must match (be subtypes of) the parameter types of + * the closure type. + * + * The `tpe` of this node is the result type of the closure type, or + * `nothing` if the latter is `nothing`. + * + * Evaluation steps are as follows: + * + * 1. Let `funV` be the result of evaluating `fun`. + * 2. If `funV` is `nullClosure`, trigger an NPE undefined behavior. + * 3. Let `argsV` be the result of evaluating `args`, in order. + * 4. Invoke `funV` with arguments `argsV`, and return the result. + */ + sealed case class ApplyTypedClosure(flags: ApplyFlags, fun: Tree, args: List[Tree])( + implicit val pos: Position) + extends Tree { + + val tpe: Type = fun.tpe match { + case ClosureType(_, resultType, _) => resultType + case NothingType => NothingType + case _ => NothingType // never a valid tree + } + } + + /** New lambda instance of a SAM class. + * + * Functionally, a `NewLambda` is equivalent to an instance of an anonymous + * class with the following shape: + * + * {{{ + * val funV: ((...Ts) => R)! = fun; + * (new superClass with interfaces { + * def () = this.superClass::() + * def methodName(...args: Ts): R = funV(...args) + * }): tpe + * }}} + * + * where `superClass`, `interfaces`, `methodName`, `Ts` and `R` are taken + * from the `descriptor`. `Ts` and `R` are the `paramTypes` and `resultType` + * of the descriptor. They are required because there is no one-to-one + * mapping between `TypeRef`s and `Type`s, and we want the shape of the + * class to be a deterministic function of the `descriptor`. + * + * The `fun` must have type `((...Ts) => R)!`. + * + * Intuitively, `tpe` must be a supertype of `superClass! & ...interfaces!`. + * Since our type system does not have intersection types, in practice this + * means that there must exist `C ∈ { superClass } ∪ interfaces` such that + * `tpe` is a supertype of `C!`. + * + * The uniqueness of the anonymous class and its run-time class name are + * not guaranteed. + */ + sealed case class NewLambda(descriptor: NewLambda.Descriptor, fun: Tree)( + val tpe: Type)( + implicit val pos: Position) + extends Tree + + object NewLambda { + final case class Descriptor(superClass: ClassName, + interfaces: List[ClassName], methodName: MethodName, + paramTypes: List[Type], resultType: Type) { + + require(paramTypes.size == methodName.paramTypeRefs.size) + + private val _hashCode: Int = { + import scala.util.hashing.MurmurHash3._ + var acc = 1546348150 // "NewLambda.Descriptor".hashCode() + acc = mix(acc, superClass.##) + acc = mix(acc, interfaces.##) + acc = mix(acc, methodName.##) + acc = mix(acc, paramTypes.##) + acc = mixLast(acc, resultType.##) + finalizeHash(acc, 5) + } + + // Overridden despite the 'case class' because we want the fail fast on different hash codes + override def equals(that: Any): Boolean = { + (this eq that.asInstanceOf[AnyRef]) || (that match { + case that: Descriptor => + this._hashCode == that._hashCode && // fail fast on different hash codes + this.superClass == that.superClass && + this.interfaces == that.interfaces && + this.methodName == that.methodName && + this.paramTypes == that.paramTypes && + this.resultType == that.resultType + case _ => + false + }) + } + + // Overridden despite the 'case class' because we want to store it + override def hashCode(): Int = _hashCode + + // Overridden despite the 'case class' because we want the better prefix string + override def toString(): String = + s"NewLambda.Descriptor($superClass, $interfaces, $methodName, $paramTypes, $resultType)" + } + } + /** Unary operation. + * + * All unary operations follow common evaluation steps: + * + * 1. Let `lhsValue` be the result of evaluating `lhs`. + * 2. Perform an operation that depends on `op` and `lhsValue`. * * The `Class_x` operations take a `jl.Class!` argument, i.e., a * non-nullable `jl.Class`. * + * Likewise, `Array_length`, `GetClass`, `Clone` and `UnwrapFromThrowable` + * take arguments of non-nullable types. + * * `CheckNotNull` throws NPEs subject to UB. * + * `Throw` always throws, obviously. + * + * `Clone` and `WrapAsThrowable` are side-effect-free but not pure. + * * Otherwise, unary operations preserve pureness. */ sealed case class UnaryOp(op: UnaryOp.Code, lhs: Tree)( @@ -341,9 +500,34 @@ object Trees { final val Class_componentType = 23 final val Class_superClass = 24 + // Misc ops introduced in 1.18, which used to have dedicated IR nodes + final val Array_length = 25 + final val GetClass = 26 + final val Clone = 27 + final val IdentityHashCode = 28 + final val WrapAsThrowable = 29 + final val UnwrapFromThrowable = 30 + final val Throw = 31 + + // Floating point bit manipulation, introduced in 1.20 + final val Float_toBits = 32 + // final val Float_toRawBits = 33 // Reserved + final val Float_fromBits = 34 + final val Double_toBits = 35 + // final val Double_toRawBits = 36 // Reserved + final val Double_fromBits = 37 + def isClassOp(op: Code): Boolean = op >= Class_name && op <= Class_superClass + def isPureOp(op: Code): Boolean = (op: @switch) match { + case CheckNotNull | Clone | WrapAsThrowable | Throw => false + case _ => true + } + + def isSideEffectFreeOp(op: Code): Boolean = + op != CheckNotNull && op != Throw + def resultTypeOf(op: Code, argType: Type): Type = (op: @switch) match { case Boolean_! | Class_isPrimitive | Class_isInterface | Class_isArray => BooleanType @@ -353,20 +537,27 @@ object Trees { ByteType case IntToShort => ShortType - case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | String_length => + case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | + String_length | Array_length | IdentityHashCode | Float_toBits => IntType - case IntToLong | DoubleToLong => + case IntToLong | DoubleToLong | Double_toBits => LongType - case DoubleToFloat | LongToFloat => + case DoubleToFloat | LongToFloat | Float_fromBits => FloatType - case IntToDouble | LongToDouble | FloatToDouble => + case IntToDouble | LongToDouble | FloatToDouble | Double_fromBits => DoubleType - case CheckNotNull => + case CheckNotNull | Clone => argType.toNonNullable case Class_name => StringType - case Class_componentType | Class_superClass => + case Class_componentType | Class_superClass | GetClass => ClassType(ClassClass, nullable = true) + case WrapAsThrowable => + ClassType(ThrowableClass, nullable = false) + case UnwrapFromThrowable => + AnyType + case Throw => + NothingType } } @@ -488,6 +679,12 @@ object Trees { final val Class_cast = 61 final val Class_newArray = 62 + // New in 1.20 + final val Int_unsigned_/ = 63 + final val Int_unsigned_% = 64 + final val Long_unsigned_/ = 65 + final val Long_unsigned_% = 66 + def isClassOp(op: Code): Boolean = op >= Class_isInstance && op <= Class_newArray @@ -502,10 +699,12 @@ object Trees { case String_+ => StringType case Int_+ | Int_- | Int_* | Int_/ | Int_% | - Int_| | Int_& | Int_^ | Int_<< | Int_>>> | Int_>> => + Int_| | Int_& | Int_^ | Int_<< | Int_>>> | Int_>> | + Int_unsigned_/ | Int_unsigned_% => IntType case Long_+ | Long_- | Long_* | Long_/ | Long_% | - Long_| | Long_& | Long_^ | Long_<< | Long_>>> | Long_>> => + Long_| | Long_& | Long_^ | Long_<< | Long_>>> | Long_>> | + Long_unsigned_/ | Long_unsigned_% => LongType case Float_+ | Float_- | Float_* | Float_/ | Float_% => FloatType @@ -530,11 +729,6 @@ object Trees { val tpe = ArrayType(typeRef, nullable = false) } - sealed case class ArrayLength(array: Tree)(implicit val pos: Position) - extends Tree { - val tpe = IntType - } - sealed case class ArraySelect(array: Tree, index: Tree)(val tpe: Type)( implicit val pos: Position) extends AssignLhs @@ -556,32 +750,6 @@ object Trees { implicit val pos: Position) extends Tree - sealed case class GetClass(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = ClassType(ClassClass, nullable = true) - } - - sealed case class Clone(expr: Tree)(implicit val pos: Position) - extends Tree { - // this is OK because our type system does not have singleton types - val tpe: Type = expr.tpe.toNonNullable - } - - sealed case class IdentityHashCode(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = IntType - } - - sealed case class WrapAsThrowable(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = ClassType(ThrowableClass, nullable = false) - } - - sealed case class UnwrapFromThrowable(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = AnyType - } - // JavaScript expressions sealed case class JSNew(ctor: Tree, args: List[TreeOrJSSpread])( @@ -734,7 +902,7 @@ object Trees { */ sealed case class JSSuperConstructorCall(args: List[TreeOrJSSpread])( implicit val pos: Position) extends Tree { - val tpe = NoType + val tpe = VoidType } /** JavaScript dynamic import of the form `import(arg)`. @@ -827,14 +995,10 @@ object Trees { implicit val pos: Position) extends Tree { - val tpe = NoType // cannot be in expression position + val tpe = VoidType } - /** Unary operation (always preserves pureness). - * - * Operations which do not preserve pureness are not allowed in this tree. - * These are notably ++ and -- - */ + /** JavaScript unary operation. */ sealed case class JSUnaryOp(op: JSUnaryOp.Code, lhs: Tree)( implicit val pos: Position) extends Tree { val tpe = JSUnaryOp.resultTypeOf(op) @@ -855,11 +1019,7 @@ object Trees { AnyType } - /** Binary operation (always preserves pureness). - * - * Operations which do not preserve pureness are not allowed in this tree. - * These are notably +=, -=, *=, /= and %= - */ + /** JavaScript binary operation. */ sealed case class JSBinaryOp(op: JSBinaryOp.Code, lhs: Tree, rhs: Tree)( implicit val pos: Position) extends Tree { val tpe = JSBinaryOp.resultTypeOf(op) @@ -966,8 +1126,15 @@ object Trees { * [[isJSIdentifierName]]) *and* it is not reserved (see * [[ReservedJSIdentifierNames]]). */ - def isValidJSGlobalRefName(name: String): Boolean = - isJSIdentifierName(name) && !ReservedJSIdentifierNames.contains(name) + def isValidJSGlobalRefName(name: String): Boolean = { + (isJSIdentifierName(name) && !ReservedJSIdentifierNames.contains(name)) || + name == FileLevelThis + } + + /** The JavaScript value that is an alias to `this` + * at the top-level of the generated file. + */ + final val FileLevelThis = "this" } sealed case class JSTypeOfGlobalRef(globalRef: JSGlobalRef)( @@ -975,10 +1142,6 @@ object Trees { val tpe = AnyType } - sealed case class JSLinkingInfo()(implicit val pos: Position) extends Tree { - val tpe = AnyType - } - // Literals /** Marker for literals. Literals are always pure. @@ -1072,26 +1235,62 @@ object Trees { val tpe = ClassType(ClassClass, nullable = false) } + sealed case class LinkTimeProperty(name: String)(val tpe: Type)( + implicit val pos: Position) + extends Tree + + object LinkTimeProperty { + final val ProductionMode = "core/productionMode" + final val ESVersion = "core/esVersion" + final val UseECMAScript2015Semantics = "core/useECMAScript2015Semantics" + final val IsWebAssembly = "core/isWebAssembly" + final val LinkerVersion = "core/linkerVersion" + } + // Atomic expressions - sealed case class VarRef(ident: LocalIdent)(val tpe: Type)( + sealed case class VarRef(name: LocalName)(val tpe: Type)( implicit val pos: Position) extends AssignLhs - sealed case class This()(val tpe: Type)(implicit val pos: Position) - extends Tree + /** Convenience constructor and extractor for `VarRef`s representing `this` bindings. */ + object This { + def apply()(tpe: Type)(implicit pos: Position): VarRef = + VarRef(LocalName.This)(tpe) + + def unapply(tree: VarRef): Boolean = + tree.name.isThis + } /** Closure with explicit captures. * - * @param arrow - * If `true`, the closure is an Arrow Function (`=>`), which does not have - * an `this` parameter, and cannot be constructed (called with `new`). - * If `false`, it is a regular Function (`function`). + * If `flags.typed` is `true`, this is a typed closure with a `ClosureType`. + * In that case, `flags.arrow` must be `true`, and `restParam` must be + * `None`. Typed closures cannot be passed to JavaScript code. This is + * enforced at the type system level, since `ClosureType`s are not subtypes + * of `AnyType`. The only meaningful operation one can perform on a typed + * closure is to call it using `ApplyTypedClosure`. + * + * If `flags.typed` is `false`, this is a JavaScript closure with type + * `AnyNotNullType`. In that case, the `ptpe` or all `params` and + * `resultType` must all be `AnyType`. + * + * If `flags.arrow` is `true`, the closure is an Arrow Function (`=>`), + * which does not have a `this` parameter, and cannot be constructed (called + * with `new`). If `false`, it is a regular Function (`function`), which + * does have a `this` parameter of type `AnyType`. Typed closures are always + * Arrow functions, since they do not have a `this` parameter. + * + * 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`. */ - sealed case class Closure(arrow: Boolean, captureParams: List[ParamDef], - params: List[ParamDef], restParam: Option[ParamDef], body: Tree, - captureValues: List[Tree])( + sealed case class Closure(flags: ClosureFlags, captureParams: List[ParamDef], + params: List[ParamDef], restParam: Option[ParamDef], resultType: Type, + body: Tree, captureValues: List[Tree])( implicit val pos: Position) extends Tree { - val tpe = AnyNotNullType + val tpe: Type = + if (flags.typed) ClosureType(params.map(_.ptpe), resultType, nullable = false) + else AnyNotNullType } /** Creates a JavaScript class value. @@ -1144,7 +1343,7 @@ object Trees { * * Implementations should transform contained trees and potentially adjust the result. */ - def transform(transformer: Transformers.Transformer, isStat: Boolean)( + def transform(transformer: Transformers.Transformer)( implicit pos: Position): Tree /** Prints the IR representation of this transient node. @@ -1436,6 +1635,60 @@ object Trees { flags.bits } + final class ClosureFlags private (private val bits: Int) extends AnyVal { + import ClosureFlags._ + + def arrow: Boolean = (bits & ArrowBit) != 0 + + def typed: Boolean = (bits & TypedBit) != 0 + + def async: Boolean = (bits & AsyncBit) != 0 + + def withArrow(arrow: Boolean): ClosureFlags = + if (arrow) new ClosureFlags(bits | ArrowBit) + else new ClosureFlags(bits & ~ArrowBit) + + def withTyped(typed: Boolean): ClosureFlags = + if (typed) new ClosureFlags(bits | TypedBit) + else new ClosureFlags(bits & ~TypedBit) + + def withAsync(async: Boolean): ClosureFlags = + if (async) new ClosureFlags(bits | AsyncBit) + else new ClosureFlags(bits & ~AsyncBit) + } + + object ClosureFlags { + /* The Arrow flag is assigned bit 0 for the serialized encoding to be + * directly compatible with the `arrow` parameter from IR < v1.19. + */ + private final val ArrowShift = 0 + private final val ArrowBit = 1 << ArrowShift + + private final val TypedShift = 1 + private final val TypedBit = 1 << TypedShift + + private final val AsyncShift = 2 + private final val AsyncBit = 1 << AsyncShift + + /** `function` closure base flags. */ + final val function: ClosureFlags = + new ClosureFlags(0) + + /** Arrow `=>` closure base flags. */ + final val arrow: ClosureFlags = + new ClosureFlags(ArrowBit) + + /** Base flags for a typed closure. */ + final val typed: ClosureFlags = + new ClosureFlags(ArrowBit | TypedBit) + + private[ir] def fromBits(bits: Byte): ClosureFlags = + new ClosureFlags(bits & 0xff) + + private[ir] def toBits(flags: ClosureFlags): Byte = + flags.bits.toByte + } + final class MemberNamespace private ( val ordinal: Int) // intentionally public extends AnyVal { diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Types.scala b/ir/shared/src/main/scala/org/scalajs/ir/Types.scala index 000583a135..0fde4f7e37 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Types.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Types.scala @@ -12,8 +12,6 @@ package org.scalajs.ir -import scala.annotation.tailrec - import Names._ import Trees._ @@ -39,16 +37,17 @@ object Types { /** Is `null` an admissible value of this type? */ def isNullable: Boolean = this match { - case AnyType | NullType => true - case ClassType(_, nullable) => nullable - case ArrayType(_, nullable) => nullable - case _ => false + case AnyType | NullType => true + case ClassType(_, nullable) => nullable + case ArrayType(_, nullable) => nullable + case ClosureType(_, _, nullable) => nullable + case _ => false } /** A type that accepts the same values as this type except `null`, unless - * this type is `NoType`. + * this type is `VoidType`. * - * If `this` is `NoType`, returns this type. + * If `this` is `VoidType`, returns this type. * * For all other types `tpe`, `tpe.toNonNullable.isNullable` is `false`. */ @@ -62,29 +61,27 @@ object Types { } } - sealed abstract class PrimTypeWithRef extends PrimType { - def primRef: PrimRef = this match { - case NoType => VoidRef - case BooleanType => BooleanRef - case CharType => CharRef - case ByteType => ByteRef - case ShortType => ShortRef - case IntType => IntRef - case LongType => LongRef - case FloatType => FloatRef - case DoubleType => DoubleRef - case NullType => NullRef - case NothingType => NothingRef - } + /* Each PrimTypeWithRef creates its corresponding `PrimRef`. Therefore, it + * takes the parameters that need to be passed to the `PrimRef` constructor. + * This little dance ensures proper initialization safety between + * `PrimTypeWithRef`s and `PrimRef`s. + */ + sealed abstract class PrimTypeWithRef(primRefCharCode: Char, primRefDisplayName: String) + extends PrimType { + val primRef: PrimRef = new PrimRef(this, primRefCharCode, primRefDisplayName) } - /** Any type (the top type of this type system). - * A variable of this type can contain any value, including `undefined` - * and `null` and any JS value. This type supports a very limited set - * of Scala operations, the ones common to all values. Basically only - * reference equality tests and instance tests. It also supports all - * JavaScript operations, since all Scala objects are also genuine - * JavaScript objects. + /** Any type. + * + * This is the supertype of all value types that can be passed to JavaScript + * code. Record types are the canonical counter-example: they are not + * subtypes of `any` because their values cannot be given to JavaScript. + * + * This type supports a very limited set of Scala operations, the ones + * common to all values. Basically only reference equality tests and + * instance tests. It also supports all JavaScript operations, since all + * Scala objects are also genuine JavaScript values. + * * The type java.lang.Object in the back-end maps to [[AnyType]] because it * can hold JS values (not only instances of Scala.js classes). */ @@ -102,7 +99,7 @@ object Types { * Expressions from which one can never come back are typed as `Nothing`. * For example, `throw` and `return`. */ - case object NothingType extends PrimTypeWithRef + case object NothingType extends PrimTypeWithRef('E', "nothing") /** The type of `undefined`. */ case object UndefType extends PrimType @@ -110,42 +107,42 @@ object Types { /** Boolean type. * It does not accept `null` nor `undefined`. */ - case object BooleanType extends PrimTypeWithRef + case object BooleanType extends PrimTypeWithRef('Z', "boolean") /** `Char` type, a 16-bit UTF-16 code unit. * It does not accept `null` nor `undefined`. */ - case object CharType extends PrimTypeWithRef + case object CharType extends PrimTypeWithRef('C', "char") /** 8-bit signed integer type. * It does not accept `null` nor `undefined`. */ - case object ByteType extends PrimTypeWithRef + case object ByteType extends PrimTypeWithRef('B', "byte") /** 16-bit signed integer type. * It does not accept `null` nor `undefined`. */ - case object ShortType extends PrimTypeWithRef + case object ShortType extends PrimTypeWithRef('S', "short") /** 32-bit signed integer type. * It does not accept `null` nor `undefined`. */ - case object IntType extends PrimTypeWithRef + case object IntType extends PrimTypeWithRef('I', "int") /** 64-bit signed integer type. * It does not accept `null` nor `undefined`. */ - case object LongType extends PrimTypeWithRef + case object LongType extends PrimTypeWithRef('J', "long") /** Float type (32-bit). * It does not accept `null` nor `undefined`. */ - case object FloatType extends PrimTypeWithRef + case object FloatType extends PrimTypeWithRef('F', "float") /** Double type (64-bit). * It does not accept `null` nor `undefined`. */ - case object DoubleType extends PrimTypeWithRef + case object DoubleType extends PrimTypeWithRef('D', "double") /** String type. * It does not accept `null` nor `undefined`. @@ -156,7 +153,7 @@ object Types { * It does not accept `undefined`. * The null type is a subtype of all class types and array types. */ - case object NullType extends PrimTypeWithRef + case object NullType extends PrimTypeWithRef('N', "null") /** Class (or interface) type. */ final case class ClassType(className: ClassName, nullable: Boolean) extends Type { @@ -178,12 +175,52 @@ object Types { def toNonNullable: ArrayType = ArrayType(arrayTypeRef, nullable = false) } + /** Closure type. + * + * This is the type of a typed closure. Parameters and result are + * statically typed according to the `closureTypeRef` components. + * + * Closure types may be nullable. `Null()` is a valid value of a nullable + * closure type. This is unfortunately required to have default values of + * closure types, which in turn is required to be used as the type of a + * field. + * + * Closure types are non-variant in both parameter and result types. + * + * Closure types are *not* subtypes of `AnyType`. That statically prevents + * them from going into JavaScript code or in any other universal context. + * They do not support type tests nor casts. + * + * The following subtyping relationships hold for any closure type `CT`: + * {{{ + * nothing <: CT <: void + * }}} + * For a nullable closure type `CT`, additionally the following subtyping + * relationship holds: + * {{{ + * null <: CT + * }}} + */ + final case class ClosureType(paramTypes: List[Type], resultType: Type, + nullable: Boolean) extends Type { + def toNonNullable: ClosureType = + ClosureType(paramTypes, resultType, nullable = false) + } + /** Record type. + * * Used by the optimizer to inline classes as records with multiple fields. * They are desugared as several local variables by JSDesugaring. * Record types cannot cross method boundaries, so they cannot appear as * the type of fields or parameters, nor as result types of methods. * The compiler itself never generates record types. + * + * Record types currently do not feature any form of subtyping. For R1 to be + * a subtype of R2, it must have the same fields, in the same order, with + * equivalent types. + * + * Record types are not subtypes of `any`. As such, they can never be passed + * to JavaScript. */ final case class RecordType(fields: List[RecordType.Field]) extends Type { def findField(name: SimpleFieldName): RecordType.Field = @@ -197,8 +234,11 @@ object Types { tpe: Type, mutable: Boolean) } - /** No type. */ - case object NoType extends PrimTypeWithRef + /** Void type, the top of type of our type system. */ + case object VoidType extends PrimTypeWithRef('V', "void") + + @deprecated("Use VoidType instead", since = "1.18.0") + lazy val NoType: VoidType.type = VoidType /** Type reference (allowed for classOf[], is/asInstanceOf[]). * @@ -224,18 +264,25 @@ object Types { } case thiz: ClassRef => that match { - case that: ClassRef => thiz.className.compareTo(that.className) - case that: PrimRef => 1 - case that: ArrayTypeRef => -1 + case that: ClassRef => thiz.className.compareTo(that.className) + case _: PrimRef => 1 + case _ => -1 } case thiz: ArrayTypeRef => that match { case that: ArrayTypeRef => if (thiz.dimensions != that.dimensions) thiz.dimensions - that.dimensions else thiz.base.compareTo(that.base) + case _: TransientTypeRef => + -1 case _ => 1 } + case thiz: TransientTypeRef => + that match { + case that: TransientTypeRef => thiz.name.compareTo(that.name) + case _ => 1 + } } def show(): String = { @@ -250,8 +297,12 @@ object Types { sealed abstract class NonArrayTypeRef extends TypeRef + // scalastyle:off equals.hash.code + // PrimRef uses reference equality, but has a stable hashCode() method + /** Primitive type reference. */ - final case class PrimRef private[ir] (tpe: PrimTypeWithRef) + final class PrimRef private[Types] (val tpe: PrimTypeWithRef, + charCodeInit: Char, displayNameInit: String) // "Init" variants so we can have good Scaladoc on the val's extends NonArrayTypeRef { /** The display name of this primitive type. @@ -263,19 +314,7 @@ object Types { * For `NullType` and `NothingType`, the names are `"null"` and * `"nothing"`, respectively. */ - val displayName: String = tpe match { - case NoType => "void" - case BooleanType => "boolean" - case CharType => "char" - case ByteType => "byte" - case ShortType => "short" - case IntType => "int" - case LongType => "long" - case FloatType => "float" - case DoubleType => "double" - case NullType => "null" - case NothingType => "nothing" - } + val displayName: String = displayNameInit /** The char code of this primitive type. * @@ -287,41 +326,30 @@ object Types { * For `NullType` and `NothingType`, the char codes are `'N'` and `'E'`, * respectively. */ - val charCode: Char = tpe match { - case NoType => 'V' - case BooleanType => 'Z' - case CharType => 'C' - case ByteType => 'B' - case ShortType => 'S' - case IntType => 'I' - case LongType => 'J' - case FloatType => 'F' - case DoubleType => 'D' - case NullType => 'N' - case NothingType => 'E' - } + val charCode: Char = charCodeInit + + // Stable hash code, corresponding to reference equality + override def hashCode(): Int = charCode.## } - /* @unchecked for the initialization checker of Scala 3 - * When we get here, `NoType` is not yet considered fully initialized because - * its method `primRef` can access `VoidRef`. Since the constructor of - * `PrimRef` pattern-matches on its `tpe`, which is `NoType`, this is flagged - * by the init checker, although our usage is safe given that we do not call - * `primRef`. The same reasoning applies to the other primitive types. - * In the future, we may want to rearrange the initialization sequence of - * this file to avoid this issue. - */ - final val VoidRef = PrimRef(NoType: @unchecked) - final val BooleanRef = PrimRef(BooleanType: @unchecked) - final val CharRef = PrimRef(CharType: @unchecked) - final val ByteRef = PrimRef(ByteType: @unchecked) - final val ShortRef = PrimRef(ShortType: @unchecked) - final val IntRef = PrimRef(IntType: @unchecked) - final val LongRef = PrimRef(LongType: @unchecked) - final val FloatRef = PrimRef(FloatType: @unchecked) - final val DoubleRef = PrimRef(DoubleType: @unchecked) - final val NullRef = PrimRef(NullType: @unchecked) - final val NothingRef = PrimRef(NothingType: @unchecked) + // scalastyle:on equals.hash.code + + object PrimRef { + def unapply(typeRef: PrimRef): Some[PrimTypeWithRef] = + Some(typeRef.tpe) + } + + final val VoidRef = VoidType.primRef + final val BooleanRef = BooleanType.primRef + final val CharRef = CharType.primRef + final val ByteRef = ByteType.primRef + final val ShortRef = ShortType.primRef + final val IntRef = IntType.primRef + final val LongRef = LongType.primRef + final val FloatRef = FloatType.primRef + final val DoubleRef = DoubleType.primRef + final val NullRef = NullType.primRef + final val NothingRef = NothingType.primRef /** Class (or interface) type. */ final case class ClassRef(className: ClassName) extends NonArrayTypeRef { @@ -337,11 +365,28 @@ object Types { object ArrayTypeRef { def of(innerType: TypeRef): ArrayTypeRef = innerType match { - case innerType: NonArrayTypeRef => ArrayTypeRef(innerType, 1) - case ArrayTypeRef(base, dim) => ArrayTypeRef(base, dim + 1) + case innerType: NonArrayTypeRef => ArrayTypeRef(innerType, 1) + case ArrayTypeRef(base, dim) => ArrayTypeRef(base, dim + 1) + case innerType: TransientTypeRef => throw new IllegalArgumentException(innerType.toString()) } } + /** Transient TypeRef to store any type as a method parameter or result type. + * + * `TransientTypeRef` cannot be serialized. It is only used in the linker to + * support some of its desugarings and/or optimizations. + * + * `TransientTypeRef`s cannot be used for methods in the `Public` namespace. + * + * The `name` is used for equality, hashing, and sorting. It is assumed that + * all occurrences of a `TransientTypeRef` with the same `name` associated + * to an enclosing method namespace (enclosing class, member namespace and + * simple method name) have the same `tpe`. + */ + final case class TransientTypeRef(name: LabelName)(val tpe: Type) extends TypeRef { + def displayName: String = name.nameString + } + /** Generates a literal zero of the given type. */ def zeroOf(tpe: Type)(implicit pos: Position): Tree = tpe match { case BooleanType => BooleanLiteral(false) @@ -355,32 +400,18 @@ object Types { case StringType => StringLiteral("") case UndefType => Undefined() - case NullType | AnyType | ClassType(_, true) | ArrayType(_, true) => Null() + case NullType | AnyType | ClassType(_, true) | ArrayType(_, true) | + ClosureType(_, _, true) => + Null() case tpe: RecordType => RecordValue(tpe, tpe.fields.map(f => zeroOf(f.tpe))) - case NothingType | NoType | ClassType(_, false) | ArrayType(_, false) | - AnyNotNullType => + case NothingType | VoidType | ClassType(_, false) | ArrayType(_, false) | + ClosureType(_, _, false) | AnyNotNullType => throw new IllegalArgumentException(s"cannot generate a zero for $tpe") } - val BoxedClassToPrimType: Map[ClassName, PrimType] = Map( - BoxedUnitClass -> UndefType, - BoxedBooleanClass -> BooleanType, - BoxedCharacterClass -> CharType, - BoxedByteClass -> ByteType, - BoxedShortClass -> ShortType, - BoxedIntegerClass -> IntType, - BoxedLongClass -> LongType, - BoxedFloatClass -> FloatType, - BoxedDoubleClass -> DoubleType, - BoxedStringClass -> StringType - ) - - val PrimTypeToBoxedClass: Map[PrimType, ClassName] = - BoxedClassToPrimType.map(_.swap) - /** Tests whether a type `lhs` is a subtype of `rhs` (or equal). * @param isSubclass A function testing whether a class/interface is a * subclass of another class/interface. @@ -388,17 +419,36 @@ object Types { def isSubtype(lhs: Type, rhs: Type)( isSubclass: (ClassName, ClassName) => Boolean): Boolean = { + /* It is fine to use WellKnownNames here because nothing in `Names` nor + * `Types` calls `isSubtype`. So this code path is not reached during their + * initialization. + */ + import WellKnownNames.{AncestorsOfPseudoArrayClass, ObjectClass, PrimTypeToBoxedClass} + def isSubnullable(lhs: Boolean, rhs: Boolean): Boolean = rhs || !lhs (lhs == rhs) || ((lhs, rhs) match { - case (_, NoType) => true - case (NoType, _) => false - case (_, AnyType) => true case (NothingType, _) => true + case (_, VoidType) => true + case (VoidType, _) => false + + case (NullType, _) => rhs.isNullable + + case (ClosureType(lhsParamTypes, lhsResultType, lhsNullable), + ClosureType(rhsParamTypes, rhsResultType, rhsNullable)) => + isSubnullable(lhsNullable, rhsNullable) && + lhsParamTypes == rhsParamTypes && + lhsResultType == rhsResultType + + case (_: ClosureType, _) => false + case (_, _: ClosureType) => false + + case (_: RecordType, _) => false + case (_, _: RecordType) => false - case (NullType, _) => rhs.isNullable + case (_, AnyType) => true case (_, AnyNotNullType) => !lhs.isNullable case (ClassType(lhsClass, lhsNullable), ClassType(rhsClass, rhsNullable)) => diff --git a/ir/shared/src/main/scala/org/scalajs/ir/WellKnownNames.scala b/ir/shared/src/main/scala/org/scalajs/ir/WellKnownNames.scala new file mode 100644 index 0000000000..cc85cd47da --- /dev/null +++ b/ir/shared/src/main/scala/org/scalajs/ir/WellKnownNames.scala @@ -0,0 +1,158 @@ +/* + * 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 org.scalajs.ir + +import Names._ +import Types._ + +/** Names for "well-known" classes and methods. + * + * Well-known classes and methods have a dedicated meaning in the semantics of + * the IR. For example, `java.lang.Class` is well-known because it is the type + * of `ClassOf` nodes. + */ +object WellKnownNames { + + /** `java.lang.Object`, the root of the class hierarchy. */ + val ObjectClass: ClassName = ClassName("java.lang.Object") + + /** `ClassRef(ObjectClass)`. */ + val ObjectRef: ClassRef = ClassRef(ObjectClass) + + // Hijacked classes + val BoxedUnitClass: ClassName = ClassName("java.lang.Void") + val BoxedBooleanClass: ClassName = ClassName("java.lang.Boolean") + val BoxedCharacterClass: ClassName = ClassName("java.lang.Character") + val BoxedByteClass: ClassName = ClassName("java.lang.Byte") + val BoxedShortClass: ClassName = ClassName("java.lang.Short") + val BoxedIntegerClass: ClassName = ClassName("java.lang.Integer") + val BoxedLongClass: ClassName = ClassName("java.lang.Long") + val BoxedFloatClass: ClassName = ClassName("java.lang.Float") + val BoxedDoubleClass: ClassName = ClassName("java.lang.Double") + val BoxedStringClass: ClassName = ClassName("java.lang.String") + + /** The set of all hijacked classes. */ + val HijackedClasses: Set[ClassName] = Set( + BoxedUnitClass, + BoxedBooleanClass, + BoxedCharacterClass, + BoxedByteClass, + BoxedShortClass, + BoxedIntegerClass, + BoxedLongClass, + BoxedFloatClass, + BoxedDoubleClass, + BoxedStringClass + ) + + /** Map from hijacked classes to their respective primitive types. */ + val BoxedClassToPrimType: Map[ClassName, PrimType] = Map( + BoxedUnitClass -> UndefType, + BoxedBooleanClass -> BooleanType, + BoxedCharacterClass -> CharType, + BoxedByteClass -> ByteType, + BoxedShortClass -> ShortType, + BoxedIntegerClass -> IntType, + BoxedLongClass -> LongType, + BoxedFloatClass -> FloatType, + BoxedDoubleClass -> DoubleType, + BoxedStringClass -> StringType + ) + + /** Map from primitive types to their respective boxed (hijacked) classes. */ + val PrimTypeToBoxedClass: Map[PrimType, ClassName] = + BoxedClassToPrimType.map(_.swap) + + /** The class of things returned by `ClassOf` and `GetClass`. */ + val ClassClass: ClassName = ClassName("java.lang.Class") + + /** `java.lang.Cloneable`, which is an ancestor of array classes and is used + * by `Clone`. + */ + val CloneableClass: ClassName = ClassName("java.lang.Cloneable") + + /** `java.io.Serializable`, which is an ancestor of array classes. */ + val SerializableClass: ClassName = ClassName("java.io.Serializable") + + /** The superclass of all throwables. + * + * This is the result type of `WrapAsThrowable` nodes, as well as the input + * type of `UnwrapFromThrowable`. + */ + val ThrowableClass = ClassName("java.lang.Throwable") + + /** The exception thrown by a division by 0. */ + val ArithmeticExceptionClass: ClassName = + ClassName("java.lang.ArithmeticException") + + /** The exception thrown by an `ArraySelect` that is out of bounds. */ + val ArrayIndexOutOfBoundsExceptionClass: ClassName = + ClassName("java.lang.ArrayIndexOutOfBoundsException") + + /** The exception thrown by an `Assign(ArraySelect, ...)` where the value cannot be stored. */ + val ArrayStoreExceptionClass: ClassName = + ClassName("java.lang.ArrayStoreException") + + /** The exception thrown by a `NewArray(...)` with a negative size. */ + val NegativeArraySizeExceptionClass: ClassName = + ClassName("java.lang.NegativeArraySizeException") + + /** The exception thrown by a variety of nodes for `null` arguments. + * + * - `Apply` and `ApplyStatically` for the receiver, + * - `Select` for the qualifier, + * - `ArrayLength` and `ArraySelect` for the array, + * - `GetClass`, `Clone` and `UnwrapFromException` for their respective only arguments. + */ + val NullPointerExceptionClass: ClassName = + ClassName("java.lang.NullPointerException") + + /** The exception thrown by a `BinaryOp.String_charAt` that is out of bounds. */ + val StringIndexOutOfBoundsExceptionClass: ClassName = + ClassName("java.lang.StringIndexOutOfBoundsException") + + /** The exception thrown by an `AsInstanceOf` that fails. */ + val ClassCastExceptionClass: ClassName = + ClassName("java.lang.ClassCastException") + + /** The exception thrown by a `Class_newArray` if the first argument is `classOf[Unit]`. */ + val IllegalArgumentExceptionClass: ClassName = + ClassName("java.lang.IllegalArgumentException") + + /** The set of classes and interfaces that are ancestors of array classes. */ + private[ir] val AncestorsOfPseudoArrayClass: Set[ClassName] = { + /* This would logically be defined in Types, but that introduces a cyclic + * dependency between the initialization of Names and Types. + */ + Set(ObjectClass, CloneableClass, SerializableClass) + } + + /** Name of a constructor without argument. + * + * This is notably the signature of constructors of module classes. + */ + final val NoArgConstructorName: MethodName = + MethodName.constructor(Nil) + + /** Name of the static initializer method. */ + final val StaticInitializerName: MethodName = + MethodName(SimpleMethodName.StaticInitializer, Nil, VoidRef) + + /** Name of the class initializer method. */ + final val ClassInitializerName: MethodName = + MethodName(SimpleMethodName.ClassInitializer, Nil, VoidRef) + + /** ModuleID of the default module */ + final val DefaultModuleID: String = "main" + +} diff --git a/ir/shared/src/test/scala/org/scalajs/ir/HashersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/HashersTest.scala index 4115c19c81..612174ffff 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/HashersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/HashersTest.scala @@ -38,11 +38,9 @@ class HashersTest { out.close() out.toByteArray() } + val actualString = actualBytes.map(b => "%02x".format(b & 0xff)).mkString - val expectedBytes = expected.grouped(2) - .map(Integer.parseInt(_, 16).toByte).toArray - - assertArrayEquals(expectedBytes, actualBytes) + assertEquals(expected, actualString) } private val bodyWithInterestingStuff = Block( @@ -93,7 +91,7 @@ class HashersTest { ) test( - "309805e5680ffa1804811ff5c9ebc77e91846957", + "82df9d6beb7df0ee9f501380323bdb2038cc50cb", MethodDef(MemberFlags.empty, mIIMethodName, NON, List(ParamDef("x", NON, IntType, mutable = false)), IntType, Some(bodyWithInterestingStuff))( @@ -108,7 +106,7 @@ class HashersTest { } test( - "c0f1ef1b22fd1cfdc9bba78bf3e0f433e9f82fc1", + "d0fa6c753502e3d1df34e53ca6f6afb5cbdcd9d4", JSMethodDef(MemberFlags.empty, s("m"), List(ParamDef("x", NON, AnyType, mutable = false)), None, bodyWithInterestingStuff)( diff --git a/ir/shared/src/test/scala/org/scalajs/ir/NamesTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/NamesTest.scala new file mode 100644 index 0000000000..c0667c3b93 --- /dev/null +++ b/ir/shared/src/test/scala/org/scalajs/ir/NamesTest.scala @@ -0,0 +1,77 @@ +/* + * 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 org.scalajs.ir + +import org.junit.Test +import org.junit.Assert._ + +import Names._ +import Types._ +import WellKnownNames._ + +class NamesTest { + @Test def nameStringLocalName(): Unit = { + assertEquals("foo", LocalName("foo").nameString) + assertEquals(".this", LocalName.This.nameString) + } + + @Test def nameStringLabelName(): Unit = { + assertEquals("foo", LabelName("foo").nameString) + } + + @Test def nameStringSimpleFieldName(): Unit = { + assertEquals("foo", SimpleFieldName("foo").nameString) + } + + @Test def nameStringFieldName(): Unit = { + assertEquals("a.B::foo", + FieldName(ClassName("a.B"), SimpleFieldName("foo")).nameString) + } + + @Test def nameStringSimpleMethodName(): Unit = { + assertEquals("foo", SimpleMethodName("foo").nameString) + assertEquals("", SimpleMethodName.Constructor.nameString) + assertEquals("", SimpleMethodName.StaticInitializer.nameString) + assertEquals("", SimpleMethodName.ClassInitializer.nameString) + } + + @Test def nameStringMethodName(): Unit = { + assertEquals("foo;I", MethodName("foo", Nil, IntRef).nameString) + assertEquals("foo;Z;I", MethodName("foo", List(BooleanRef), IntRef).nameString) + assertEquals("foo;Z;V", MethodName("foo", List(BooleanRef), VoidRef).nameString) + + assertEquals("foo;S;Ljava.io.Serializable;V", + MethodName("foo", List(ShortRef, ClassRef(SerializableClass)), VoidRef).nameString) + + assertEquals(";I;V", MethodName.constructor(List(IntRef)).nameString) + + assertEquals("foo;Z;R", MethodName.reflectiveProxy("foo", List(BooleanRef)).nameString) + + val refAndNameStrings: List[(TypeRef, String)] = List( + ClassRef(ObjectClass) -> "Ljava.lang.Object", + ClassRef(SerializableClass) -> "Ljava.io.Serializable", + ClassRef(BoxedStringClass) -> "Ljava.lang.String", + ArrayTypeRef(ClassRef(ObjectClass), 2) -> "[[Ljava.lang.Object", + ArrayTypeRef(ShortRef, 1) -> "[S", + TransientTypeRef(LabelName("bar"))(CharType) -> "tbar" + ) + for ((ref, nameString) <- refAndNameStrings) { + assertEquals(s"foo;$nameString;V", + MethodName("foo", List(ref), VoidRef).nameString) + } + } + + @Test def nameStringClassName(): Unit = { + assertEquals("a.B", ClassName("a.B").nameString) + } +} diff --git a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala index 57d80c3373..d570445fc8 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -22,6 +22,7 @@ import OriginalName.NoOriginalName import Printers._ import Trees._ import Types._ +import WellKnownNames._ import TestIRBuilder._ @@ -52,7 +53,7 @@ class PrintersTest { assertPrintEquals("any", AnyType) assertPrintEquals("any!", AnyNotNullType) assertPrintEquals("nothing", NothingType) - assertPrintEquals("void", UndefType) + assertPrintEquals("undef", UndefType) assertPrintEquals("boolean", BooleanType) assertPrintEquals("char", CharType) assertPrintEquals("byte", ByteType) @@ -63,7 +64,7 @@ class PrintersTest { assertPrintEquals("double", DoubleType) assertPrintEquals("string", StringType) assertPrintEquals("null", NullType) - assertPrintEquals("", NoType) + assertPrintEquals("void", VoidType) assertPrintEquals("java.lang.Object", ClassType(ObjectClass, nullable = true)) assertPrintEquals("java.lang.String!", @@ -74,6 +75,10 @@ class PrintersTest { assertPrintEquals("java.lang.String[]!", ArrayType(ArrayTypeRef(BoxedStringClass, 1), nullable = false)) + assertPrintEquals("(() => int)", ClosureType(Nil, IntType, nullable = true)) + assertPrintEquals("((any, java.lang.String!) => boolean)!", + ClosureType(List(AnyType, ClassType(BoxedStringClass, nullable = false)), BooleanType, nullable = false)) + assertPrintEquals("(x: int, var y: any)", RecordType(List( RecordType.Field("x", NON, IntType, mutable = false), @@ -85,6 +90,8 @@ class PrintersTest { assertPrintEquals("java.lang.Object[]", ArrayTypeRef(ObjectClass, 1)) assertPrintEquals("int[][]", ArrayTypeRef(IntRef, 2)) + + assertPrintEquals("foo", TransientTypeRef(LabelName("foo"))(IntType)) } @Test def printVarDef(): Unit = { @@ -127,7 +134,7 @@ class PrintersTest { | 6 |} """, - Labeled("lab", NoType, i(6))) + Labeled("lab", VoidType, i(6))) assertPrintEquals( """ @@ -144,7 +151,7 @@ class PrintersTest { | 6 |} """, - Labeled("lab", NoType, Block(i(5), i(6)))) + Labeled("lab", VoidType, Block(i(5), i(6)))) } @Test def printAssign(): Unit = { @@ -154,6 +161,7 @@ class PrintersTest { @Test def printReturn(): Unit = { assertPrintEquals("return@lab 5", Return(i(5), "lab")) + assertPrintEquals("return@lab", Return(Skip(), "lab")) } @Test def printIf(): Unit = { @@ -173,7 +181,7 @@ class PrintersTest { | 5 |} """, - If(b(true), i(5), Skip())(NoType)) + If(b(true), i(5), Skip())(VoidType)) assertPrintEquals( """ @@ -194,6 +202,61 @@ class PrintersTest { If(ref("x", BooleanType), ref("y", BooleanType), b(false))(BooleanType)) } + @Test def printLinkTimeIf(): Unit = { + assertPrintEquals( + """ + |link-time-if (true) { + | 5 + |} else { + | 6 + |} + """, + LinkTimeIf(b(true), i(5), i(6))(IntType)) + + assertPrintEquals( + """ + |link-time-if (true) { + | 5 + |} else { + |} + """, + LinkTimeIf(b(true), i(5), Skip())(VoidType)) + + assertPrintEquals( + """ + |link-time-if (true) { + | 5 + |} else { + | link-time-if (false) { + | 6 + | } else { + | 7 + | } + |} + """, + LinkTimeIf(b(true), i(5), LinkTimeIf(b(false), i(6), i(7))(IntType))(IntType)) + + assertPrintEquals( + """ + |link-time-if (x) { + | true + |} else { + | y + |} + """, + LinkTimeIf(ref("x", BooleanType), b(true), ref("y", BooleanType))(BooleanType)) + + assertPrintEquals( + """ + |link-time-if (x) { + | y + |} else { + | false + |} + """, + LinkTimeIf(ref("x", BooleanType), ref("y", BooleanType), b(false))(BooleanType)) + } + @Test def printWhile(): Unit = { assertPrintEquals( """ @@ -266,10 +329,6 @@ class PrintersTest { TryFinally(TryCatch(i(5), "e", NON, i(6))(IntType), i(7))) } - @Test def printThrow(): Unit = { - assertPrintEquals("throw null", Throw(Null())) - } - @Test def printMatch(): Unit = { assertPrintEquals( """ @@ -291,6 +350,10 @@ class PrintersTest { i(11))(IntType)) } + @Test def printJSAwait(): Unit = { + assertPrintEquals("await(p)", JSAwait(ref("p", AnyType))) + } + @Test def printDebugger(): Unit = { assertPrintEquals("debugger", Debugger()) } @@ -322,7 +385,7 @@ class PrintersTest { @Test def printApply(): Unit = { assertPrintEquals("x.m;V()", - Apply(EAF, ref("x", "test.Test"), MethodName("m", Nil, V), Nil)(NoType)) + Apply(EAF, ref("x", "test.Test"), MethodName("m", Nil, V), Nil)(VoidType)) assertPrintEquals("x.m;I;I(5)", Apply(EAF, ref("x", "test.Test"), MethodName("m", List(I), I), List(i(5)))(IntType)) @@ -334,7 +397,7 @@ class PrintersTest { @Test def printApplyStatically(): Unit = { assertPrintEquals("x.test.Test::m;V()", ApplyStatically(EAF, ref("x", "test.Test"), "test.Test", - MethodName("m", Nil, V), Nil)(NoType)) + MethodName("m", Nil, V), Nil)(VoidType)) assertPrintEquals("x.test.Test::m;I;I(5)", ApplyStatically(EAF, ref("x", "test.Test"), "test.Test", MethodName("m", List(I), I), List(i(5)))(IntType)) @@ -344,12 +407,12 @@ class PrintersTest { assertPrintEquals("x.test.Test::private::m;V()", ApplyStatically(EAF.withPrivate(true), ref("x", "test.Test"), - "test.Test", MethodName("m", Nil, V), Nil)(NoType)) + "test.Test", MethodName("m", Nil, V), Nil)(VoidType)) } @Test def printApplyStatic(): Unit = { assertPrintEquals("test.Test::m;V()", - ApplyStatic(EAF, "test.Test", MethodName("m", Nil, V), Nil)(NoType)) + ApplyStatic(EAF, "test.Test", MethodName("m", Nil, V), Nil)(VoidType)) assertPrintEquals("test.Test::m;I;I(5)", ApplyStatic(EAF, "test.Test", MethodName("m", List(I), I), List(i(5)))(IntType)) @@ -359,7 +422,7 @@ class PrintersTest { assertPrintEquals("test.Test::private::m;V()", ApplyStatic(EAF.withPrivate(true), "test.Test", MethodName("m", Nil, V), - Nil)(NoType)) + Nil)(VoidType)) } @Test def printApplyDynamicImportStatic(): Unit = { @@ -367,6 +430,47 @@ class PrintersTest { ApplyDynamicImport(EAF, "test.Test", MethodName("m", Nil, O), Nil)) } + @Test def printApplyTypedClosure(): Unit = { + assertPrintEquals("f()", + ApplyTypedClosure(EAF, ref("f", NothingType), Nil)) + assertPrintEquals("f(1)", + ApplyTypedClosure(EAF, ref("f", NothingType), List(i(1)))) + assertPrintEquals("f(1, 2)", + ApplyTypedClosure(EAF, ref("f", NothingType), List(i(1), i(2)))) + } + + @Test def printNewLambda(): Unit = { + assertPrintEquals( + s""" + |( + | extends java.lang.Object implements java.lang.Comparable, + | def compareTo;Ljava.lang.Object;Z(any): boolean, + | (typed-lambda<>(that: any): boolean = { + | true + | }) + |) + """, + NewLambda( + NewLambda.Descriptor( + ObjectClass, + List("java.lang.Comparable"), + MethodName(SimpleMethodName("compareTo"), List(ClassRef(ObjectClass)), BooleanRef), + List(AnyType), + BooleanType + ), + Closure( + ClosureFlags.typed, + Nil, + List(ParamDef("that", NON, AnyType, mutable = false)), + None, + BooleanType, + BooleanLiteral(true), + Nil + ) + )(ClassType("java.lang.Comparable", nullable = false)) + ) + } + @Test def printUnaryOp(): Unit = { import UnaryOp._ @@ -402,6 +506,19 @@ class PrintersTest { assertPrintEquals("x.isArray", UnaryOp(Class_isArray, classVarRef)) assertPrintEquals("x.componentType", UnaryOp(Class_componentType, classVarRef)) assertPrintEquals("x.superClass", UnaryOp(Class_superClass, classVarRef)) + + assertPrintEquals("x.length", UnaryOp(Array_length, ref("x", arrayType(IntRef, 1)))) + assertPrintEquals("x.getClass()", UnaryOp(GetClass, ref("x", AnyType))) + assertPrintEquals("(x)", UnaryOp(Clone, ref("x", arrayType(ObjectClass, 1)))) + assertPrintEquals("(x)", UnaryOp(IdentityHashCode, ref("x", AnyType))) + assertPrintEquals("(e)", UnaryOp(WrapAsThrowable, ref("e", AnyType))) + assertPrintEquals("(e)", + UnaryOp(UnwrapFromThrowable, ref("e", ClassType(ThrowableClass, nullable = true)))) + + assertPrintEquals("(x)", UnaryOp(Float_toBits, ref("x", FloatType))) + assertPrintEquals("(x)", UnaryOp(Float_fromBits, ref("x", IntType))) + assertPrintEquals("(x)", UnaryOp(Double_toBits, ref("x", DoubleType))) + assertPrintEquals("(x)", UnaryOp(Double_fromBits, ref("x", LongType))) } @Test def printPseudoUnaryOp(): Unit = { @@ -554,6 +671,15 @@ class PrintersTest { BinaryOp(Class_isAssignableFrom, classVarRef, ref("y", ClassType(ClassClass, nullable = false)))) assertPrintEquals("cast(x, y)", BinaryOp(Class_cast, classVarRef, ref("y", AnyType))) assertPrintEquals("newArray(x, y)", BinaryOp(Class_newArray, classVarRef, ref("y", IntType))) + + assertPrintEquals("(x unsigned_/[int] y)", + BinaryOp(Int_unsigned_/, ref("x", IntType), ref("y", IntType))) + assertPrintEquals("(x unsigned_%[int] y)", + BinaryOp(Int_unsigned_%, ref("x", IntType), ref("y", IntType))) + assertPrintEquals("(x unsigned_/[long] y)", + BinaryOp(Long_unsigned_/, ref("x", LongType), ref("y", LongType))) + assertPrintEquals("(x unsigned_%[long] y)", + BinaryOp(Long_unsigned_%, ref("x", LongType), ref("y", LongType))) } @Test def printNewArray(): Unit = { @@ -573,10 +699,6 @@ class PrintersTest { ArrayValue(ArrayTypeRef(IntRef, 2), List(Null()))) } - @Test def printArrayLength(): Unit = { - assertPrintEquals("x.length", ArrayLength(ref("x", arrayType(IntRef, 1)))) - } - @Test def printArraySelect(): Unit = { assertPrintEquals("x[3]", ArraySelect(ref("x", arrayType(IntRef, 1)), i(3))(IntType)) @@ -605,27 +727,6 @@ class PrintersTest { AsInstanceOf(ref("x", AnyType), IntType)) } - @Test def printGetClass(): Unit = { - assertPrintEquals("x.getClass()", GetClass(ref("x", AnyType))) - } - - @Test def printClone(): Unit = { - assertPrintEquals("(x)", Clone(ref("x", arrayType(ObjectClass, 1)))) - } - - @Test def printIdentityHashCode(): Unit = { - assertPrintEquals("(x)", IdentityHashCode(ref("x", AnyType))) - } - - @Test def printWrapAsThrowable(): Unit = { - assertPrintEquals("(e)", WrapAsThrowable(ref("e", AnyType))) - } - - @Test def printUnwrapFromThrowable(): Unit = { - assertPrintEquals("(e)", - UnwrapFromThrowable(ref("e", ClassType(ThrowableClass, nullable = true)))) - } - @Test def printJSNew(): Unit = { assertPrintEquals("new C()", JSNew(ref("C", AnyType), Nil)) assertPrintEquals("new C(4, 5)", JSNew(ref("C", AnyType), List(i(4), i(5)))) @@ -807,12 +908,8 @@ class PrintersTest { assertPrintEquals("(typeof global:Foo)", JSTypeOfGlobalRef(JSGlobalRef("Foo"))) } - @Test def printJSLinkingInfo(): Unit = { - assertPrintEquals("", JSLinkingInfo()) - } - @Test def printUndefined(): Unit = { - assertPrintEquals("(void 0)", Undefined()) + assertPrintEquals("undefined", Undefined()) } @Test def printNull(): Unit = { @@ -887,9 +984,6 @@ class PrintersTest { @Test def printVarRef(): Unit = { assertPrintEquals("x", VarRef("x")(IntType)) - } - - @Test def printThis(): Unit = { assertPrintEquals("this", This()(AnyType)) } @@ -900,7 +994,7 @@ class PrintersTest { | 5 |}) """, - Closure(false, Nil, Nil, None, i(5), Nil)) + Closure(ClosureFlags.function, Nil, Nil, None, AnyType, i(5), Nil)) assertPrintEquals( """ @@ -909,12 +1003,13 @@ class PrintersTest { |}) """, Closure( - true, + ClosureFlags.arrow, List( ParamDef("x", NON, AnyType, mutable = false), ParamDef("y", TestON, IntType, mutable = false)), List(ParamDef("z", NON, AnyType, mutable = false)), None, + AnyType, ref("z", AnyType), List(ref("a", IntType), i(6)))) @@ -924,9 +1019,54 @@ class PrintersTest { | z |}) """, - Closure(false, Nil, Nil, + Closure(ClosureFlags.function, Nil, Nil, Some(ParamDef("z", NON, AnyType, mutable = false)), - ref("z", AnyType), Nil)) + AnyType, ref("z", AnyType), Nil)) + + assertPrintEquals( + """ + |(async lambda<>(...z: any): any = { + | z + |}) + """, + Closure(ClosureFlags.function.withAsync(true), Nil, Nil, + Some(ParamDef("z", NON, AnyType, mutable = false)), + AnyType, ref("z", AnyType), Nil)) + + assertPrintEquals( + """ + |(async arrow-lambda<>(...z: any): any = { + | z + |}) + """, + Closure(ClosureFlags.arrow.withAsync(true), Nil, Nil, + Some(ParamDef("z", NON, AnyType, mutable = false)), + AnyType, ref("z", AnyType), Nil)) + + assertPrintEquals( + """ + |(typed-lambda<>() { + | 5 + |}) + """, + Closure(ClosureFlags.typed, Nil, Nil, None, VoidType, i(5), Nil)) + + assertPrintEquals( + """ + |(typed-lambda(z: int): int = { + | z + |}) + """, + Closure( + ClosureFlags.typed, + List( + ParamDef("x", NON, AnyType, mutable = false), + ParamDef("y", TestON, IntType, mutable = false)), + List(ParamDef("z", NON, IntType, mutable = false)), + None, + IntType, + ref("z", IntType), + List(ref("a", IntType), i(6)))) } @Test def printCreateJSClass(): Unit = { @@ -937,13 +1077,21 @@ class PrintersTest { CreateJSClass("Foo", List(ref("x", IntType), ref("y", AnyType)))) } + @Test def printLinkTimeProperty(): Unit = { + assertPrintEquals( + """ + |(foo) + """, + LinkTimeProperty("foo")(StringType)) + } + @Test def printTransient(): Unit = { class MyTransient(expr: Tree) extends Transient.Value { val tpe: Type = AnyType def traverse(traverser: Traversers.Traverser): Unit = ??? - def transform(transformer: Transformers.Transformer, isStat: Boolean)( + def transform(transformer: Transformers.Transformer)( implicit pos: Position): Tree = ??? def printIR(out: Printers.IRTreePrinter): Unit = { @@ -1247,7 +1395,7 @@ class PrintersTest { """, MethodDef(MemberFlags.empty, mIVMethodName, NON, List(ParamDef("x", NON, IntType, mutable = false)), - NoType, Some(i(5)))(NoOptHints, UNV)) + VoidType, Some(i(5)))(NoOptHints, UNV)) assertPrintEquals( """ @@ -1294,7 +1442,7 @@ class PrintersTest { |constructor def constructor(x: any): any = { | 5; | super(6); - | (void 0) + | undefined |} """, JSConstructorDef(MemberFlags.empty.withNamespace(Constructor), diff --git a/ir/shared/src/test/scala/org/scalajs/ir/SerializersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/SerializersTest.scala new file mode 100644 index 0000000000..a8c18507d9 --- /dev/null +++ b/ir/shared/src/test/scala/org/scalajs/ir/SerializersTest.scala @@ -0,0 +1,61 @@ +/* + * 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 org.scalajs.ir + +import org.junit.Test +import org.junit.Assert._ + +class SerializersTest { + @Test def testHacksUseBelow(): Unit = { + import Serializers.Hacks + + val hacks1_0 = new Hacks("1.0") + assertFalse(hacks1_0.useBelow(0)) + assertTrue(hacks1_0.useBelow(1)) + assertTrue(hacks1_0.useBelow(5)) + assertTrue(hacks1_0.useBelow(15)) + assertTrue(hacks1_0.useBelow(1000)) + + val hacks1_7 = new Hacks("1.7") + assertFalse(hacks1_7.useBelow(0)) + assertFalse(hacks1_7.useBelow(1)) + assertFalse(hacks1_7.useBelow(5)) + assertFalse(hacks1_7.useBelow(7)) + assertTrue(hacks1_7.useBelow(8)) + assertTrue(hacks1_7.useBelow(15)) + assertTrue(hacks1_7.useBelow(1000)) + + val hacks1_50 = new Hacks("1.50") + assertFalse(hacks1_50.useBelow(0)) + assertFalse(hacks1_50.useBelow(1)) + assertFalse(hacks1_50.useBelow(5)) + assertFalse(hacks1_50.useBelow(15)) + assertTrue(hacks1_50.useBelow(1000)) + + // Non-stable versions never get any hacks + val hacks1_9_snapshot = new Hacks("1.9-SNAPSHOT") + assertFalse(hacks1_9_snapshot.useBelow(0)) + assertFalse(hacks1_9_snapshot.useBelow(1)) + assertFalse(hacks1_9_snapshot.useBelow(5)) + assertFalse(hacks1_9_snapshot.useBelow(15)) + assertFalse(hacks1_9_snapshot.useBelow(1000)) + + // Incompatible versions never get any hacks + val hacks2_5 = new Hacks("2.5") + assertFalse(hacks2_5.useBelow(0)) + assertFalse(hacks2_5.useBelow(1)) + assertFalse(hacks2_5.useBelow(5)) + assertFalse(hacks2_5.useBelow(15)) + assertFalse(hacks2_5.useBelow(1000)) + } +} diff --git a/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala b/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala index d89d8050a7..ac2e9cecd0 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala @@ -19,6 +19,7 @@ import OriginalName.NoOriginalName import Printers._ import Trees._ import Types._ +import WellKnownNames._ object TestIRBuilder { @@ -37,6 +38,10 @@ object TestIRBuilder { val NoOptHints = OptimizerHints.empty // String -> Name conversions + implicit def string2localName(name: String): LocalName = + LocalName(name) + implicit def string2labelName(name: String): LabelName = + LabelName(name) implicit def string2simpleFieldName(name: String): SimpleFieldName = SimpleFieldName(name) implicit def string2className(name: String): ClassName = @@ -45,8 +50,6 @@ object TestIRBuilder { // String -> Ident conversions implicit def string2localIdent(name: String): LocalIdent = LocalIdent(LocalName(name)) - implicit def string2labelIdent(name: String): LabelIdent = - LabelIdent(LabelName(name)) implicit def string2simpleFieldIdent(name: String): SimpleFieldIdent = SimpleFieldIdent(SimpleFieldName(name)) implicit def string2classIdent(name: String): ClassIdent = @@ -79,7 +82,7 @@ object TestIRBuilder { def d(value: Double): DoubleLiteral = DoubleLiteral(value) def s(value: String): StringLiteral = StringLiteral(value) - def ref(ident: LocalIdent, tpe: Type): VarRef = VarRef(ident)(tpe) + def ref(name: LocalName, tpe: Type): VarRef = VarRef(name)(tpe) def arrayType(base: NonArrayTypeRef, dimensions: Int): ArrayType = ArrayType(ArrayTypeRef(base, dimensions), nullable = true) diff --git a/javalib/src/main/scala/java/io/PrintStream.scala b/javalib/src/main/scala/java/io/PrintStream.scala index d6141609c3..7868abd03c 100644 --- a/javalib/src/main/scala/java/io/PrintStream.scala +++ b/javalib/src/main/scala/java/io/PrintStream.scala @@ -79,9 +79,9 @@ class PrintStream private (_out: OutputStream, autoFlush: Boolean, private var errorFlag: Boolean = false override def flush(): Unit = - ensureOpenAndTrapIOExceptions(out.flush()) + ensureOpenAndTrapIOExceptions(() => out.flush()) - override def close(): Unit = trapIOExceptions { + override def close(): Unit = trapIOExceptions { () => if (!closing) { closing = true encoder.close() @@ -133,7 +133,7 @@ class PrintStream private (_out: OutputStream, autoFlush: Boolean, */ override def write(b: Int): Unit = { - ensureOpenAndTrapIOExceptions { + ensureOpenAndTrapIOExceptions { () => out.write(b) if (autoFlush && b == '\n') flush() @@ -141,7 +141,7 @@ class PrintStream private (_out: OutputStream, autoFlush: Boolean, } override def write(buf: Array[Byte], off: Int, len: Int): Unit = { - ensureOpenAndTrapIOExceptions { + ensureOpenAndTrapIOExceptions { () => out.write(buf, off, len) if (autoFlush) flush() @@ -157,17 +157,17 @@ class PrintStream private (_out: OutputStream, autoFlush: Boolean, def print(s: String): Unit = printString(if (s == null) "null" else s) def print(obj: AnyRef): Unit = printString(String.valueOf(obj)) - private def printString(s: String): Unit = ensureOpenAndTrapIOExceptions { + private def printString(s: String): Unit = ensureOpenAndTrapIOExceptions { () => encoder.write(s) encoder.flushBuffer() } - def print(s: Array[Char]): Unit = ensureOpenAndTrapIOExceptions { + def print(s: Array[Char]): Unit = ensureOpenAndTrapIOExceptions { () => encoder.write(s) encoder.flushBuffer() } - def println(): Unit = ensureOpenAndTrapIOExceptions { + def println(): Unit = ensureOpenAndTrapIOExceptions { () => encoder.write('\n') // In Scala.js the line separator is always LF encoder.flushBuffer() if (autoFlush) @@ -214,15 +214,15 @@ class PrintStream private (_out: OutputStream, autoFlush: Boolean, this } - @inline private[this] def trapIOExceptions(body: => Unit): Unit = { + @inline private[this] def trapIOExceptions(body: Runnable): Unit = { try { - body + body.run() } catch { case _: IOException => setError() } } - @inline private[this] def ensureOpenAndTrapIOExceptions(body: => Unit): Unit = { + @inline private[this] def ensureOpenAndTrapIOExceptions(body: Runnable): Unit = { if (closed) setError() else trapIOExceptions(body) } diff --git a/javalib/src/main/scala/java/io/PrintWriter.scala b/javalib/src/main/scala/java/io/PrintWriter.scala index 5e3facf333..57833e56f5 100644 --- a/javalib/src/main/scala/java/io/PrintWriter.scala +++ b/javalib/src/main/scala/java/io/PrintWriter.scala @@ -41,9 +41,9 @@ class PrintWriter(protected[io] var out: Writer, private var errorFlag: Boolean = false def flush(): Unit = - ensureOpenAndTrapIOExceptions(out.flush()) + ensureOpenAndTrapIOExceptions(() => out.flush()) - def close(): Unit = trapIOExceptions { + def close(): Unit = trapIOExceptions { () => if (!closed) { flush() closed = true @@ -76,19 +76,19 @@ class PrintWriter(protected[io] var out: Writer, protected[io] def clearError(): Unit = errorFlag = false override def write(c: Int): Unit = - ensureOpenAndTrapIOExceptions(out.write(c)) + ensureOpenAndTrapIOExceptions(() => out.write(c)) override def write(buf: Array[Char], off: Int, len: Int): Unit = - ensureOpenAndTrapIOExceptions(out.write(buf, off, len)) + ensureOpenAndTrapIOExceptions(() => out.write(buf, off, len)) override def write(buf: Array[Char]): Unit = - ensureOpenAndTrapIOExceptions(out.write(buf)) + ensureOpenAndTrapIOExceptions(() => out.write(buf)) override def write(s: String, off: Int, len: Int): Unit = - ensureOpenAndTrapIOExceptions(out.write(s, off, len)) + ensureOpenAndTrapIOExceptions(() => out.write(s, off, len)) override def write(s: String): Unit = - ensureOpenAndTrapIOExceptions(out.write(s)) + ensureOpenAndTrapIOExceptions(() => out.write(s)) def print(b: Boolean): Unit = write(String.valueOf(b)) def print(c: Char): Unit = write(c) @@ -147,15 +147,15 @@ class PrintWriter(protected[io] var out: Writer, this } - @inline private[this] def trapIOExceptions(body: => Unit): Unit = { + @inline private[this] def trapIOExceptions(body: Runnable): Unit = { try { - body + body.run() } catch { case _: IOException => setError() } } - @inline private[this] def ensureOpenAndTrapIOExceptions(body: => Unit): Unit = { + @inline private[this] def ensureOpenAndTrapIOExceptions(body: Runnable): Unit = { if (closed) setError() else trapIOExceptions(body) } diff --git a/javalib/src/main/scala/java/lang/Character.scala b/javalib/src/main/scala/java/lang/Character.scala index 50c38793ac..a085f427d7 100644 --- a/javalib/src/main/scala/java/lang/Character.scala +++ b/javalib/src/main/scala/java/lang/Character.scala @@ -15,7 +15,7 @@ package java.lang import scala.annotation.{tailrec, switch} import scala.scalajs.js -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import scala.scalajs.LinkingInfo.ESVersion import java.lang.constant.Constable @@ -128,7 +128,7 @@ object Character { if (!isValidCodePoint(codePoint)) throw new IllegalArgumentException() - if (linkingInfo.esVersion >= ESVersion.ES2015) { + if (LinkingInfo.esVersion >= ESVersion.ES2015) { js.Dynamic.global.String.fromCodePoint(codePoint).asInstanceOf[String] } else { if (codePoint < MIN_SUPPLEMENTARY_CODE_POINT) { diff --git a/javalib/src/main/scala/java/lang/ClassValue.scala b/javalib/src/main/scala/java/lang/ClassValue.scala index 94f46965e4..0ab92d37cb 100644 --- a/javalib/src/main/scala/java/lang/ClassValue.scala +++ b/javalib/src/main/scala/java/lang/ClassValue.scala @@ -16,14 +16,14 @@ import java.util.HashMap import scala.scalajs.js import scala.scalajs.js.annotation._ -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import scala.scalajs.LinkingInfo.ESVersion import Utils._ abstract class ClassValue[T] protected () { private val jsMap: js.Map[Class[_], T] = { - if (linkingInfo.esVersion >= ESVersion.ES2015 || js.typeOf(js.Dynamic.global.Map) != "undefined") + if (LinkingInfo.esVersion >= ESVersion.ES2015 || js.typeOf(js.Dynamic.global.Map) != "undefined") new js.Map() else null @@ -35,7 +35,7 @@ abstract class ClassValue[T] protected () { * emitting ES 2015 code, which allows to dead-code-eliminate the branches * using `HashMap`s, and therefore `HashMap` itself. */ - linkingInfo.esVersion >= ESVersion.ES2015 || jsMap != null + LinkingInfo.esVersion >= ESVersion.ES2015 || jsMap != null } /* We use a HashMap instead of an IdentityHashMap because the latter is @@ -50,7 +50,7 @@ abstract class ClassValue[T] protected () { def get(`type`: Class[_]): T = { if (useJSMap) { - mapGetOrElseUpdate(jsMap, `type`)(computeValue(`type`)) + mapGetOrElseUpdate(jsMap, `type`)(() => computeValue(`type`)) } else { /* We first perform `get`, and if the result is null, we use * `containsKey` to disambiguate a present null from an absent key. diff --git a/javalib/src/main/scala/java/lang/Double.scala b/javalib/src/main/scala/java/lang/Double.scala index e2768f8aca..b8c1ffc779 100644 --- a/javalib/src/main/scala/java/lang/Double.scala +++ b/javalib/src/main/scala/java/lang/Double.scala @@ -15,7 +15,7 @@ package java.lang import java.lang.constant.{Constable, ConstantDesc} import scala.scalajs.js -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import Utils._ @@ -364,14 +364,28 @@ object Double { @inline def isFinite(d: scala.Double): scala.Boolean = !isNaN(d) && !isInfinite(d) + /** Hash code of a number (excluding Longs). + * + * Because of the common encoding for integer and floating point values, + * the hashCode of Floats and Doubles must align with that of Ints for the + * common values. + * + * For other values, we use the hashCode specified by the JavaDoc for + * *Doubles*, even for values which are valid Float values. Because of the + * previous point, we cannot align completely with the Java specification, + * so there is no point trying to be a bit more aligned here. Always using + * the Double version requires fewer branches. + * + * We use different code paths in JS and Wasm for performance reasons. + * The two implementations compute the same results. + */ @inline def hashCode(value: scala.Double): Int = { - if (linkingInfo.isWebAssembly) + if (LinkingInfo.isWebAssembly) hashCodeForWasm(value) else - FloatingPointBits.numberHashCode(value) + hashCodeForJS(value) } - // See FloatingPointBits for the spec of this computation @inline private def hashCodeForWasm(value: scala.Double): Int = { val bits = doubleToLongBits(value) @@ -382,13 +396,20 @@ object Double { Long.hashCode(bits) } - // Wasm intrinsic + @inline + private def hashCodeForJS(value: scala.Double): Int = { + val valueInt = (value.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int] + if (valueInt.toDouble == value && 1.0/value != scala.Double.NegativeInfinity) + valueInt + else + Long.hashCode(doubleToLongBits(value)) + } + @inline def longBitsToDouble(bits: scala.Long): scala.Double = - FloatingPointBits.longBitsToDouble(bits) + throw new Error("stub") // body replaced by the compiler back-end - // Wasm intrinsic @inline def doubleToLongBits(value: scala.Double): scala.Long = - FloatingPointBits.doubleToLongBits(value) + throw new Error("stub") // body replaced by the compiler back-end @inline def sum(a: scala.Double, b: scala.Double): scala.Double = a + b diff --git a/javalib/src/main/scala/java/lang/Float.scala b/javalib/src/main/scala/java/lang/Float.scala index 6bb6bb4565..279d8ed1a8 100644 --- a/javalib/src/main/scala/java/lang/Float.scala +++ b/javalib/src/main/scala/java/lang/Float.scala @@ -13,9 +13,9 @@ package java.lang import java.lang.constant.{Constable, ConstantDesc} -import java.math.BigInteger import scala.scalajs.js +import scala.scalajs.LinkingInfo._ /* This is a hijacked class. Its instances are primitive numbers. * Constructors are not emitted. @@ -122,14 +122,14 @@ object Float { } else if (undefOrIsDefined(groups(4))) { // Decimal notation val fullNumberStr = undefOrForceGet(groups(4)) - val integralPartStr = undefOrGetOrElse(groups(5))("") - val fractionalPartStr = undefOrGetOrElse(groups(6))("") + undefOrGetOrElse(groups(7))("") - val exponentStr = undefOrGetOrElse(groups(8))("0") + val integralPartStr = undefOrGetOrElse(groups(5))(() => "") + val fractionalPartStr = undefOrGetOrElse(groups(6))(() => "") + undefOrGetOrElse(groups(7))(() => "") + val exponentStr = undefOrGetOrElse(groups(8))(() => "0") parseFloatDecimal(fullNumberStr, integralPartStr, fractionalPartStr, exponentStr) } else { // Hexadecimal notation - val integralPartStr = undefOrGetOrElse(groups(10))("") - val fractionalPartStr = undefOrGetOrElse(groups(11))("") + undefOrGetOrElse(groups(12))("") + val integralPartStr = undefOrGetOrElse(groups(10))(() => "") + val fractionalPartStr = undefOrGetOrElse(groups(11))(() => "") + undefOrGetOrElse(groups(12))(() => "") val binaryExpStr = undefOrForceGet(groups(13)) parseFloatHexadecimal(integralPartStr, fractionalPartStr, binaryExpStr) } @@ -150,14 +150,7 @@ object Float { val zDouble = z.toDouble if (zDouble == z0) { - /* This branch is always taken when strictFloats are disabled, and there - * is no Math.fround support. In that case, Floats are basically - * equivalent to Doubles, and we make no specific guarantee about the - * result, so we can quickly return `z`. - * More importantly, the computations in the `else` branch assume that - * Float operations are exact, so we must return early. - * - * This branch is also always taken when z0 is 0.0 or Infinity, which the + /* This branch is always taken when z0 is 0.0 or Infinity, which the * `else` branch assumes does not happen. */ z @@ -233,9 +226,23 @@ object Float { fractionalPartStr: String, exponentStr: String, zDown: scala.Float, zUp: scala.Float, mid: scala.Double): scala.Float = { + /* Get the best available implementation of big integers for the given platform. + * + * If JS bigint's are supported, use them. Otherwise fall back on + * `java.math.BigInteger`. + * + * We need a `linkTimeIf` here because the JS bigint implementation uses + * the `**` operator, which does not link when `esVersion < ESVersion.ES2016`. + */ + val bigIntImpl = linkTimeIf[BigIntImpl](esVersion >= ESVersion.ES2020) { + BigIntImpl.JSBigInt + } { + BigIntImpl.JBigInteger + } + // 1. Accurately parse the string with the representation f × 10ᵉ - val f: BigInteger = new BigInteger(integralPartStr + fractionalPartStr) + val f: bigIntImpl.Repr = bigIntImpl.fromString(integralPartStr + fractionalPartStr) val e: Int = Integer.parseInt(exponentStr) - fractionalPartStr.length() /* Note: we know that `e` is "reasonable" (in the range [-324, +308]). If @@ -268,24 +275,23 @@ object Float { val mExplicitBits = midBits & ((1L << mbits) - 1) val mImplicit1Bit = 1L << mbits // the implicit '1' bit of a normalized floating-point number - val m = BigInteger.valueOf(mExplicitBits | mImplicit1Bit) + val m = bigIntImpl.fromUnsignedLong53(mExplicitBits | mImplicit1Bit) val k = biasedK - bias - mbits // 3. Accurately compare f × 10ᵉ to m × 2ᵏ - @inline def compare(x: BigInteger, y: BigInteger): Int = - x.compareTo(y) + import bigIntImpl.{multiplyBy2Pow, multiplyBy10Pow} val cmp = if (e >= 0) { if (k >= 0) - compare(multiplyBy10Pow(f, e), multiplyBy2Pow(m, k)) + bigIntImpl.compare(multiplyBy10Pow(f, e), multiplyBy2Pow(m, k)) else - compare(multiplyBy2Pow(multiplyBy10Pow(f, e), -k), m) // this branch may be dead code in practice + bigIntImpl.compare(multiplyBy2Pow(multiplyBy10Pow(f, e), -k), m) // this branch may be dead code in practice } else { if (k >= 0) - compare(f, multiplyBy2Pow(multiplyBy10Pow(m, -e), k)) + bigIntImpl.compare(f, multiplyBy2Pow(multiplyBy10Pow(m, -e), k)) else - compare(multiplyBy2Pow(f, -k), multiplyBy10Pow(m, -e)) + bigIntImpl.compare(multiplyBy2Pow(f, -k), multiplyBy10Pow(m, -e)) } // 4. Choose zDown or zUp depending on the result of the comparison @@ -300,11 +306,54 @@ object Float { zUp } - @inline private def multiplyBy10Pow(v: BigInteger, e: Int): BigInteger = - v.multiply(BigInteger.TEN.pow(e)) + /** An implementation of big integer arithmetics that we need in the above method. */ + private sealed abstract class BigIntImpl { + type Repr + + def fromString(str: String): Repr + + /** Creates a big integer from a `Long` that needs at most 53 bits (unsigned). */ + def fromUnsignedLong53(x: scala.Long): Repr + + def multiplyBy2Pow(v: Repr, e: Int): Repr + def multiplyBy10Pow(v: Repr, e: Int): Repr + + def compare(x: Repr, y: Repr): Int + } + + private object BigIntImpl { + object JSBigInt extends BigIntImpl { + type Repr = js.BigInt + + @inline def fromString(str: String): Repr = js.BigInt(str) - @inline private def multiplyBy2Pow(v: BigInteger, e: Int): BigInteger = - v.shiftLeft(e) + // The 53-bit restriction guarantees that the conversion to `Double` is lossless. + @inline def fromUnsignedLong53(x: scala.Long): Repr = js.BigInt(x.toDouble) + + @inline def multiplyBy2Pow(v: Repr, e: Int): Repr = v << js.BigInt(e) + @inline def multiplyBy10Pow(v: Repr, e: Int): Repr = v * (js.BigInt(10) ** js.BigInt(e)) + + @inline def compare(x: Repr, y: Repr): Int = { + if (x < y) -1 + else if (x > y) 1 + else 0 + } + } + + object JBigInteger extends BigIntImpl { + import java.math.BigInteger + + type Repr = BigInteger + + @inline def fromString(str: String): Repr = new BigInteger(str) + @inline def fromUnsignedLong53(x: scala.Long): Repr = BigInteger.valueOf(x) + + @inline def multiplyBy2Pow(v: Repr, e: Int): Repr = v.shiftLeft(e) + @inline def multiplyBy10Pow(v: Repr, e: Int): Repr = v.multiply(BigInteger.TEN.pow(e)) + + @inline def compare(x: Repr, y: Repr): Int = x.compareTo(y) + } + } private def parseFloatHexadecimal(integralPartStr: String, fractionalPartStr: String, binaryExpStr: String): scala.Float = { @@ -378,13 +427,11 @@ object Float { @inline def hashCode(value: scala.Float): Int = Double.hashCode(value.toDouble) - // Wasm intrinsic @inline def intBitsToFloat(bits: scala.Int): scala.Float = - FloatingPointBits.intBitsToFloat(bits) + throw new Error("stub") // body replaced by the compiler back-end - // Wasm intrinsic @inline def floatToIntBits(value: scala.Float): scala.Int = - FloatingPointBits.floatToIntBits(value) + throw new Error("stub") // body replaced by the compiler back-end @inline def sum(a: scala.Float, b: scala.Float): scala.Float = a + b diff --git a/javalib/src/main/scala/java/lang/FloatingPointBits.scala b/javalib/src/main/scala/java/lang/FloatingPointBits.scala deleted file mode 100644 index 3b3f972346..0000000000 --- a/javalib/src/main/scala/java/lang/FloatingPointBits.scala +++ /dev/null @@ -1,356 +0,0 @@ -/* - * 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 java.lang - -import scala.scalajs.js -import scala.scalajs.js.Dynamic.global -import scala.scalajs.js.typedarray -import scala.scalajs.LinkingInfo.ESVersion - -/** Manipulating the bits of floating point numbers. */ -private[lang] object FloatingPointBits { - - import scala.scalajs.runtime.linkingInfo - - private[this] val _areTypedArraysSupported = { - // Here we use the `esVersion` test to dce the 4 subsequent tests - linkingInfo.esVersion >= ESVersion.ES2015 || { - js.typeOf(global.ArrayBuffer) != "undefined" && - js.typeOf(global.Int32Array) != "undefined" && - js.typeOf(global.Float32Array) != "undefined" && - js.typeOf(global.Float64Array) != "undefined" - } - } - - @inline - private def areTypedArraysSupported: scala.Boolean = { - /* We have a forwarder to the internal `val _areTypedArraysSupported` to - * be able to inline it. This achieves the following: - * * If we emit ES2015+, dce `|| _areTypedArraysSupported` and replace - * `areTypedArraysSupported` by `true` in the calling code, allowing - * polyfills in the calling code to be dce'ed in turn. - * * If we emit ES5, replace `areTypedArraysSupported` by - * `_areTypedArraysSupported` so we do not calculate it multiple times. - */ - linkingInfo.esVersion >= ESVersion.ES2015 || _areTypedArraysSupported - } - - private val arrayBuffer = - if (areTypedArraysSupported) new typedarray.ArrayBuffer(8) - else null - - private val int32Array = - if (areTypedArraysSupported) new typedarray.Int32Array(arrayBuffer, 0, 2) - else null - - private val float32Array = - if (areTypedArraysSupported) new typedarray.Float32Array(arrayBuffer, 0, 2) - else null - - private val float64Array = - if (areTypedArraysSupported) new typedarray.Float64Array(arrayBuffer, 0, 1) - else null - - private val areTypedArraysBigEndian = { - if (areTypedArraysSupported) { - int32Array(0) = 0x01020304 - (new typedarray.Int8Array(arrayBuffer, 0, 8))(0) == 0x01 - } else { - true // as good a value as any - } - } - - private val highOffset = if (areTypedArraysBigEndian) 0 else 1 - private val lowOffset = if (areTypedArraysBigEndian) 1 else 0 - - private val floatPowsOf2: js.Array[scala.Double] = - if (areTypedArraysSupported) null - else makePowsOf2(len = 1 << 8, java.lang.Float.MIN_NORMAL.toDouble) - - private val doublePowsOf2: js.Array[scala.Double] = - if (areTypedArraysSupported) null - else makePowsOf2(len = 1 << 11, java.lang.Double.MIN_NORMAL) - - private def makePowsOf2(len: Int, minNormal: scala.Double): js.Array[scala.Double] = { - val r = new js.Array[scala.Double](len) - r(0) = 0.0 - var i = 1 - var next = minNormal - while (i != len - 1) { - r(i) = next - i += 1 - next *= 2 - } - r(len - 1) = scala.Double.PositiveInfinity - r - } - - /** Hash code of a number (excluding Longs). - * - * Because of the common encoding for integer and floating point values, - * the hashCode of Floats and Doubles must align with that of Ints for the - * common values. - * - * For other values, we use the hashCode specified by the JavaDoc for - * *Doubles*, even for values which are valid Float values. Because of the - * previous point, we cannot align completely with the Java specification, - * so there is no point trying to be a bit more aligned here. Always using - * the Double version should typically be faster on VMs without fround - * support because we avoid several fround operations. - */ - def numberHashCode(value: scala.Double): Int = { - val iv = rawToInt(value) - if (iv == value && 1.0/value != scala.Double.NegativeInfinity) { - iv - } else { - /* Basically an inlined version of `Long.hashCode(doubleToLongBits(value))`, - * so that we never allocate a RuntimeLong instance (or anything, for - * that matter). - * - * In addition, in the happy path where typed arrays are supported, since - * we xor together the two Ints, it doesn't matter which one comes first - * or second, and hence we can use constants 0 and 1 instead of having an - * indirection through `highOffset` and `lowOffset`. - */ - if (areTypedArraysSupported) { - float64Array(0) = value - int32Array(0) ^ int32Array(1) - } else { - doubleHashCodePolyfill(value) - } - } - } - - @noinline - private def doubleHashCodePolyfill(value: scala.Double): Int = - Long.hashCode(doubleToLongBitsPolyfillInline(value)) - - def intBitsToFloat(bits: Int): scala.Float = { - if (areTypedArraysSupported) { - int32Array(0) = bits - float32Array(0) - } else { - intBitsToFloatPolyfill(bits).toFloat - } - } - - def floatToIntBits(value: scala.Float): Int = { - if (areTypedArraysSupported) { - float32Array(0) = value - int32Array(0) - } else { - floatToIntBitsPolyfill(value.toDouble) - } - } - - def longBitsToDouble(bits: scala.Long): scala.Double = { - if (areTypedArraysSupported) { - int32Array(highOffset) = (bits >>> 32).toInt - int32Array(lowOffset) = bits.toInt - float64Array(0) - } else { - longBitsToDoublePolyfill(bits) - } - } - - def doubleToLongBits(value: scala.Double): scala.Long = { - if (areTypedArraysSupported) { - float64Array(0) = value - ((int32Array(highOffset).toLong << 32) | - (int32Array(lowOffset).toLong & 0xffffffffL)) - } else { - doubleToLongBitsPolyfill(value) - } - } - - /* --- Polyfills for floating point bit manipulations --- - * - * Originally inspired by - * https://github.com/inexorabletash/polyfill/blob/3447582628b6e3ea81959c4d5987aa332c22d1ca/typedarray.js#L150-L264 - * - * Note that if typed arrays are not supported, it is almost certain that - * fround is not supported natively, so Float operations are extremely slow. - * - * We therefore do all computations in Doubles here, which is also more - * predictable, since the results do not depend on strict floats semantics. - */ - - private def intBitsToFloatPolyfill(bits: Int): scala.Double = { - val ebits = 8 - val fbits = 23 - val sign = (bits >> 31) | 1 // -1 or 1 - val e = (bits >> fbits) & ((1 << ebits) - 1) - val f = bits & ((1 << fbits) - 1) - decodeIEEE754(ebits, fbits, floatPowsOf2, scala.Float.MinPositiveValue, sign, e, f) - } - - private def floatToIntBitsPolyfill(value: scala.Double): Int = { - // Some constants - val ebits = 8 - val fbits = 23 - - // Determine sign bit and compute the absolute value av - val sign = if (value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)) -1 else 1 - val s = sign & scala.Int.MinValue - val av = sign * value - - // Compute e and f - val avr = forceFround(av) - val powsOf2 = this.floatPowsOf2 // local cache - val e = encodeIEEE754Exponent(ebits, powsOf2, avr) - val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, scala.Float.MinPositiveValue.toDouble, avr, e) - - // Encode - s | (e << fbits) | rawToInt(f) - } - - private def longBitsToDoublePolyfill(bits: scala.Long): scala.Double = { - val ebits = 11 - val fbits = 52 - val hifbits = fbits - 32 - val hi = (bits >>> 32).toInt - val lo = Utils.toUint(bits.toInt) - val sign = (hi >> 31) | 1 // -1 or 1 - val e = (hi >> hifbits) & ((1 << ebits) - 1) - val f = (hi & ((1 << hifbits) - 1)).toDouble * 0x100000000L.toDouble + lo - decodeIEEE754(ebits, fbits, doublePowsOf2, scala.Double.MinPositiveValue, sign, e, f) - } - - @noinline - private def doubleToLongBitsPolyfill(value: scala.Double): scala.Long = - doubleToLongBitsPolyfillInline(value) - - @inline - private def doubleToLongBitsPolyfillInline(value: scala.Double): scala.Long = { - // Some constants - val ebits = 11 - val fbits = 52 - val hifbits = fbits - 32 - - // Determine sign bit and compute the absolute value av - val sign = if (value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)) -1 else 1 - val s = sign & scala.Int.MinValue - val av = sign * value - - // Compute e and f - val powsOf2 = this.doublePowsOf2 // local cache - val e = encodeIEEE754Exponent(ebits, powsOf2, av) - val f = encodeIEEE754MantissaBits(ebits, fbits, powsOf2, scala.Double.MinPositiveValue, av, e) - - // Encode - val hi = s | (e << hifbits) | rawToInt(f / 0x100000000L.toDouble) - val lo = rawToInt(f) - (hi.toLong << 32) | (lo.toLong & 0xffffffffL) - } - - @inline - private def decodeIEEE754(ebits: Int, fbits: Int, - powsOf2: js.Array[scala.Double], minPositiveValue: scala.Double, - sign: scala.Int, e: Int, f: scala.Double): scala.Double = { - - // Some constants - val specialExponent = (1 << ebits) - 1 - val twoPowFbits = (1L << fbits).toDouble - - if (e == specialExponent) { - // Special - if (f == 0.0) - sign * scala.Double.PositiveInfinity - else - scala.Double.NaN - } else if (e > 0) { - // Normalized - sign * powsOf2(e) * (1 + f / twoPowFbits) - } else { - // Subnormal - sign * f * minPositiveValue - } - } - - /** Force rounding of `av` to fit in 32 bits (this is a manual `fround`). - * - * `av` must not be negative, i.e., `av < 0.0` must be false (it can be - * `NaN` or `Infinity`). - * - * When we use strict-float semantics, this is redundant, because the input - * came from a `Float` and is therefore guaranteed to be rounded already. - * However, here we don't know whether we use strict floats semantics or - * not, so we must always do it. This is not a big deal because, if this - * code is called, then any operation on `Float`s is calling the same code - * from the `CoreJSLib`, so doing one more such operation for - * `floatToIntBits` is negligible. - * - * TODO Remove this when we get rid of non-strict float semantics altogether. - */ - @inline - private def forceFround(av: scala.Double): scala.Double = { - // See the `fround` polyfill in CoreJSLib - val overflowThreshold = 3.4028235677973366e38 - val normalThreshold = 1.1754943508222875e-38 - if (av >= overflowThreshold) { - scala.Double.PositiveInfinity - } else if (av >= normalThreshold) { - val p = av * 536870913.0 // pow(2, 29) + 1 - p + (av - p) - } else { - val roundingFactor = scala.Double.MinPositiveValue / scala.Float.MinPositiveValue.toDouble - (av * roundingFactor) / roundingFactor - } - } - - private def encodeIEEE754Exponent(ebits: Int, - powsOf2: js.Array[scala.Double], av: scala.Double): Int = { - - /* Binary search of `av` inside `powsOf2`. - * There are exactly `ebits` iterations of this loop (11 for Double, 8 for Float). - */ - var eMin = 0 - var eMax = 1 << ebits - while (eMin + 1 < eMax) { - val e = (eMin + eMax) >> 1 - if (av < powsOf2(e)) // false when av is NaN - eMax = e - else - eMin = e - } - eMin - } - - @inline - private def encodeIEEE754MantissaBits(ebits: Int, fbits: Int, - powsOf2: js.Array[scala.Double], minPositiveValue: scala.Double, - av: scala.Double, e: Int): scala.Double = { - - // Some constants - val specialExponent = (1 << ebits) - 1 - val twoPowFbits = (1L << fbits).toDouble - - if (e == specialExponent) { - if (av != av) - (1L << (fbits - 1)).toDouble // NaN - else - 0.0 // Infinity - } else { - if (e == 0) - av / minPositiveValue // Subnormal - else - ((av / powsOf2(e)) - 1.0) * twoPowFbits // Normal - } - } - - @inline private def rawToInt(x: scala.Double): Int = { - import scala.scalajs.js.DynamicImplicits.number2dynamic - (x | 0).asInstanceOf[Int] - } - -} diff --git a/javalib/src/main/scala/java/lang/Integer.scala b/javalib/src/main/scala/java/lang/Integer.scala index 78267337da..0bf5d3561f 100644 --- a/javalib/src/main/scala/java/lang/Integer.scala +++ b/javalib/src/main/scala/java/lang/Integer.scala @@ -13,9 +13,10 @@ package java.lang import java.lang.constant.{Constable, ConstantDesc} +import java.util.function._ import scala.scalajs.js -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import scala.scalajs.LinkingInfo.ESVersion /* This is a hijacked class. Its instances are primitive numbers. @@ -132,7 +133,7 @@ object Integer { decodeGeneric(nm, valueOf(_, _)) @inline private[lang] def decodeGeneric[A](nm: String, - parse: (String, Int) => A): A = { + parse: BiFunction[String, Int, A]): A = { val len = nm.length() var i = 0 @@ -220,15 +221,11 @@ object Integer { (((t2 + (t2 >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24 } - // Wasm intrinsic @inline def divideUnsigned(dividend: Int, divisor: Int): Int = - if (divisor == 0) 0 / 0 - else asInt(asUint(dividend) / asUint(divisor)) + throw new Error("stub") // body replaced by the compiler back-end - // Wasm intrinsic @inline def remainderUnsigned(dividend: Int, divisor: Int): Int = - if (divisor == 0) 0 % 0 - else asInt(asUint(dividend) % asUint(divisor)) + throw new Error("stub") // body replaced by the compiler back-end @inline def highestOneBit(i: Int): Int = { /* The natural way of implementing this is: @@ -279,7 +276,7 @@ object Integer { // Intrinsic, fallback on actual code for non-literal in JS @inline def numberOfLeadingZeros(i: scala.Int): scala.Int = { - if (linkingInfo.esVersion >= ESVersion.ES2015) js.Math.clz32(i) + if (LinkingInfo.esVersion >= ESVersion.ES2015) js.Math.clz32(i) else clz32Dynamic(i) } diff --git a/javalib/src/main/scala/java/lang/Long.scala b/javalib/src/main/scala/java/lang/Long.scala index 0413372acf..faa69bc6d9 100644 --- a/javalib/src/main/scala/java/lang/Long.scala +++ b/javalib/src/main/scala/java/lang/Long.scala @@ -348,47 +348,11 @@ object Long { @inline def compareUnsigned(x: scala.Long, y: scala.Long): scala.Int = compare(x ^ SignBit, y ^ SignBit) - // Intrinsic, except for JS when using bigint's for longs - def divideUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = - divModUnsigned(dividend, divisor, isDivide = true) - - // Intrinsic, except for JS when using bigint's for longs - def remainderUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = - divModUnsigned(dividend, divisor, isDivide = false) - - private def divModUnsigned(a: scala.Long, b: scala.Long, - isDivide: scala.Boolean): scala.Long = { - /* This is a much simplified (and slow) version of - * RuntimeLong.unsignedDivModHelper. - */ - - if (b == 0L) - throw new ArithmeticException("/ by zero") - - var shift = numberOfLeadingZeros(b) - numberOfLeadingZeros(a) - var bShift = b << shift - - var rem = a - var quot = 0L - - /* Invariants: - * bShift == b << shift == b * 2^shift - * quot >= 0 - * 0 <= rem < 2 * bShift - * quot * b + rem == a - */ - while (shift >= 0 && rem != 0) { - if ((rem ^ SignBit) >= (bShift ^ SignBit)) { - rem -= bShift - quot |= (1L << shift) - } - shift -= 1 - bShift >>>= 1 - } + @inline def divideUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = + throw new Error("stub") // body replaced by the compiler back-end - if (isDivide) quot - else rem - } + @inline def remainderUnsigned(dividend: scala.Long, divisor: scala.Long): scala.Long = + throw new Error("stub") // body replaced by the compiler back-end @inline def highestOneBit(i: scala.Long): scala.Long = { diff --git a/javalib/src/main/scala/java/lang/Math.scala b/javalib/src/main/scala/java/lang/Math.scala index 42262255f3..7fe386b7a7 100644 --- a/javalib/src/main/scala/java/lang/Math.scala +++ b/javalib/src/main/scala/java/lang/Math.scala @@ -16,7 +16,7 @@ package lang import scala.scalajs.js import js.Dynamic.{ global => g } -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import scala.scalajs.LinkingInfo.ESVersion object Math { @@ -24,7 +24,7 @@ object Math { final val PI = 3.141592653589793 @inline private def assumingES6: scala.Boolean = - linkingInfo.esVersion >= ESVersion.ES2015 + LinkingInfo.esVersion >= ESVersion.ES2015 @inline def abs(a: scala.Int): scala.Int = if (a < 0) -a else a @inline def abs(a: scala.Long): scala.Long = if (a < 0) -a else a @@ -53,16 +53,71 @@ object Math { // Wasm intrinsic def rint(a: scala.Double): scala.Double = { - val rounded = js.Math.round(a) - val mod = a % 1.0 - // The following test is also false for specials (0's, Infinities and NaN) - if (mod == 0.5 || mod == -0.5) { - // js.Math.round(a) rounds up but we have to round to even - if (rounded % 2.0 == 0.0) rounded - else rounded - 1.0 - } else { - rounded - } + /* Is the integer-valued `x` odd? Fused by hand of `(x.toLong & 1L) != 0L`. + * Corner cases: returns false for Infinities and NaN. + */ + @inline def isOdd(x: scala.Double): scala.Boolean = + (x.asInstanceOf[js.Dynamic] & 1.asInstanceOf[js.Dynamic]).asInstanceOf[Int] != 0 + + /* js.Math.round(a) does *almost* what we want. It rounds to nearest, + * breaking ties *up*. We need to break ties to *even*. So we need to + * detect ties, and for them, detect if we rounded to odd instead of even. + * + * The reasons why the apparently simple algorithm below works are subtle, + * and vary a lot depending on the range of `a`: + * + * - a is NaN + * r is NaN, then the == is false + * -> return r + * + * - a is +-Infinity + * r == a, then == is true! but isOdd(r) is false + * -> return r + * + * - 2**53 <= abs(a) < Infinity + * r == a, r - 0.5 rounds back to a so == is true! + * fortunately, isOdd(r) is false because all a >= 2**53 are even + * -> return r + * + * - 2**52 <= abs(a) < 2**53 + * r == a (because all a's are integers in that range) + * - a is odd + * r - 0.5 rounds down (towards even) to r - 1.0 + * so a == r - 0.5 is false + * -> return r + * - a is even + * r - 0.5 rounds back up! (towards even) to r + * so a == r - 0.5 is true! + * but, isOdd(r) is false + * -> return r + * + * - 0.5 < abs(a) < 2**52 + * then -2**52 + 0.5 <= a <= 2**52 - 0.5 (because values in-between are not representable) + * since Math.round rounds *up* on ties, r is an integer in the range (-2**52, 2**52] + * r - 0.5 is therefore lossless + * so a == r - 0.5 accurately detects ties, and isOdd(r) breaks ties + * -> return `r`` or `r - 1.0` + * + * - a == +0.5 + * r == 1.0 + * a == r - 0.5 is true and isOdd(r) is true + * -> return `r - 1.0`, which is +0.0 + * + * - a == -0.5 + * r == -0.0 + * a == r - 0.5 is true and isOdd(r) is false + * -> return `r`, which is -0.0 + * + * - 0.0 <= abs(a) < 0.5 + * r == 0.0 with the same sign as a + * a == r - 0.5 is false + * -> return r + */ + val r = js.Math.round(a) + if ((a == r - 0.5) && isOdd(r)) + r - 1.0 + else + r } @inline def round(a: scala.Float): scala.Int = js.Math.round(a).toInt @@ -407,6 +462,43 @@ object Math { if (a >= Integer.MIN_VALUE && a <= Integer.MAX_VALUE) a.toInt else throw new ArithmeticException("Integer overflow") + // RuntimeLong intrinsic + @inline + def multiplyFull(x: scala.Int, y: scala.Int): scala.Long = + x.toLong * y.toLong + + @inline + def multiplyHigh(x: scala.Long, y: scala.Long): scala.Long = { + /* Hacker's Delight, Section 8-2, Figure 8-2, + * where we have "inlined" all the variables used only once to help our + * optimizer perform simplifications. + */ + + val x0 = x & 0xffffffffL + val x1 = x >> 32 + val y0 = y & 0xffffffffL + val y1 = y >> 32 + + val t = x1 * y0 + ((x0 * y0) >>> 32) + x1 * y1 + (t >> 32) + (((t & 0xffffffffL) + x0 * y1) >> 32) + } + + @inline + def unsignedMultiplyHigh(x: scala.Long, y: scala.Long): scala.Long = { + /* Hacker's Delight, Section 8-2: + * > For an unsigned version, simply change all the int declarations to unsigned. + * In Scala, that means changing all the >> into >>>. + */ + + val x0 = x & 0xffffffffL + val x1 = x >>> 32 + val y0 = y & 0xffffffffL + val y1 = y >>> 32 + + val t = x1 * y0 + ((x0 * y0) >>> 32) + x1 * y1 + (t >>> 32) + (((t & 0xffffffffL) + x0 * y1) >>> 32) + } + def floorDiv(a: scala.Int, b: scala.Int): scala.Int = { val quot = a / b if ((a < 0) == (b < 0) || quot * b == a) quot diff --git a/javalib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala index 76b3d067e7..31960903b9 100644 --- a/javalib/src/main/scala/java/lang/StackTrace.scala +++ b/javalib/src/main/scala/java/lang/StackTrace.scala @@ -126,7 +126,7 @@ private[lang] object StackTrace { trace.push(new StackTraceElement(classAndMethodName(0), classAndMethodName(1), undefOrForceGet(mtch(2)), parseInt(undefOrForceGet(mtch(3))), - undefOrFold(mtch(4))(-1)(parseInt(_)))) + undefOrFold(mtch(4))(() => -1)(parseInt(_)))) } else { // just in case // (explicitly use the constructor with column number so that STE has an inlineable init) @@ -413,7 +413,7 @@ private[lang] object StackTrace { while (i < len) { val mtch = lineRE.exec(lines(i)) if (mtch ne null) { - val fnName = undefOrGetOrElse(mtch(3))("{anonymous}") + val fnName = undefOrGetOrElse(mtch(3))(() => "{anonymous}") result.push( fnName + "()@" + undefOrForceGet(mtch(2)) + ":" + undefOrForceGet(mtch(1)) @@ -438,7 +438,7 @@ private[lang] object StackTrace { while (i < len) { val mtch = lineRE.exec(lines(i)) if (mtch ne null) { - val fnName = undefOrFold(mtch(1))("global code")(_ + "()") + val fnName = undefOrFold(mtch(1))(() => "global code")(_ + "()") result.push(fnName + "@" + undefOrForceGet(mtch(2)) + ":" + undefOrForceGet(mtch(3))) } i += 1 @@ -458,7 +458,7 @@ private[lang] object StackTrace { val mtch = lineRE.exec(lines(i)) if (mtch ne null) { val location = undefOrForceGet(mtch(4)) + ":" + undefOrForceGet(mtch(1)) + ":" + undefOrForceGet(mtch(2)) - val fnName0 = undefOrGetOrElse(mtch(2))("global code") + val fnName0 = undefOrGetOrElse(mtch(2))(() => "global code") val fnName = fnName0 .jsReplace("""""".re, "$1") .jsReplace("""""".re, "{anonymous}") diff --git a/javalib/src/main/scala/java/lang/System.scala b/javalib/src/main/scala/java/lang/System.scala index c7424a218a..d6ee87996f 100644 --- a/javalib/src/main/scala/java/lang/System.scala +++ b/javalib/src/main/scala/java/lang/System.scala @@ -16,9 +16,10 @@ import java.io._ import scala.scalajs.js import scala.scalajs.js.Dynamic.global -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import java.{util => ju} +import java.util.function._ object System { /* System contains a bag of unrelated features. If we naively implement @@ -70,7 +71,7 @@ object System { (new js.Date).getTime().toLong private object NanoTime { - val getHighPrecisionTime: () => scala.Double = { + val getHighPrecisionTime: js.Function0[scala.Double] = { import js.DynamicImplicits.truthValue if (js.typeOf(global.performance) != "undefined") { @@ -102,27 +103,27 @@ object System { def mismatch(): Nothing = throw new ArrayStoreException("Incompatible array types") - def impl(srcLen: Int, destLen: Int, f: (Int, Int) => Any): Unit = { + def impl(srcLen: Int, destLen: Int, f: BiConsumer[Int, Int]): Unit = { /* Perform dummy swaps to trigger an ArrayIndexOutOfBoundsException or * UBE if the positions / lengths are bad. */ if (srcPos < 0 || destPos < 0) - f(destPos, srcPos) + f.accept(destPos, srcPos) if (length < 0) - f(length, length) + f.accept(length, length) if (srcPos > srcLen - length || destPos > destLen - length) - f(destPos + length, srcPos + length) + f.accept(destPos + length, srcPos + length) if ((src ne dest) || destPos < srcPos || srcPos + length < destPos) { var i = 0 while (i < length) { - f(i + destPos, i + srcPos) + f.accept(i + destPos, i + srcPos) i += 1 } } else { var i = length - 1 while (i >= 0) { - f(i + destPos, i + srcPos) + f.accept(i + destPos, i + srcPos) i -= 1 } } @@ -183,7 +184,7 @@ object System { @inline def identityHashCode(x: Any): scala.Int = - scala.scalajs.runtime.identityHashCode(x.asInstanceOf[AnyRef]) + throw new Error("stub") // body replaced by the compiler back-end // System properties -------------------------------------------------------- @@ -200,7 +201,7 @@ object System { dictSet(result, "java.vm.specification.vendor", "Oracle Corporation") dictSet(result, "java.vm.specification.name", "Java Virtual Machine Specification") dictSet(result, "java.vm.name", "Scala.js") - dictSet(result, "java.vm.version", linkingInfo.linkerVersion) + dictSet(result, "java.vm.version", LinkingInfo.linkerVersion) dictSet(result, "java.specification.version", "1.8") dictSet(result, "java.specification.vendor", "Oracle Corporation") dictSet(result, "java.specification.name", "Java Platform API Specification") @@ -233,11 +234,11 @@ object System { } def getProperty(key: String): String = - if (dict ne null) dictGetOrElse(dict, key)(null) + if (dict ne null) dictGetOrElse(dict, key)(() => null) else properties.getProperty(key) def getProperty(key: String, default: String): String = - if (dict ne null) dictGetOrElse(dict, key)(default) + if (dict ne null) dictGetOrElse(dict, key)(() => default) else properties.getProperty(key, default) def clearProperty(key: String): String = diff --git a/javalib/src/main/scala/java/lang/Throwables.scala b/javalib/src/main/scala/java/lang/Throwables.scala index 38b17384b9..af7701641f 100644 --- a/javalib/src/main/scala/java/lang/Throwables.scala +++ b/javalib/src/main/scala/java/lang/Throwables.scala @@ -12,6 +12,8 @@ package java.lang +import java.util.function._ + import scala.scalajs.js import scala.scalajs.js.annotation.JSExport @@ -80,21 +82,21 @@ class Throwable protected (s: String, private var e: Throwable, def printStackTrace(s: java.io.PrintWriter): Unit = printStackTraceImpl(s.println(_)) - private[this] def printStackTraceImpl(sprintln: String => Unit): Unit = { + private[this] def printStackTraceImpl(sprintln: Consumer[String]): Unit = { getStackTrace() // will init it if still null // Message - sprintln(toString) + sprintln.accept(toString) // Trace if (stackTrace.length != 0) { var i = 0 while (i < stackTrace.length) { - sprintln(s" at ${stackTrace(i)}") + sprintln.accept(s" at ${stackTrace(i)}") i += 1 } } else { - sprintln(" ") + sprintln.accept(" ") } // Causes @@ -107,7 +109,7 @@ class Throwable protected (s: String, private var e: Throwable, val thisLength = thisTrace.length val parentLength = parentTrace.length - sprintln(s"Caused by: $wCause") + sprintln.accept(s"Caused by: $wCause") if (thisLength != 0) { /* Count how many frames are shared between this stack trace and the @@ -129,14 +131,14 @@ class Throwable protected (s: String, private var e: Throwable, val lengthToPrint = thisLength - sameFrameCount var i = 0 while (i < lengthToPrint) { - sprintln(s" at ${thisTrace(i)}") + sprintln.accept(s" at ${thisTrace(i)}") i += 1 } if (sameFrameCount > 0) - sprintln(s" ... $sameFrameCount more") + sprintln.accept(s" ... $sameFrameCount more") } else { - sprintln(" ") + sprintln.accept(" ") } } } diff --git a/javalib/src/main/scala/java/lang/Utils.scala b/javalib/src/main/scala/java/lang/Utils.scala index b57bc2a520..94d323e026 100644 --- a/javalib/src/main/scala/java/lang/Utils.scala +++ b/javalib/src/main/scala/java/lang/Utils.scala @@ -14,6 +14,8 @@ package java.lang import scala.language.implicitConversions +import java.util.function._ + import scala.scalajs.js import scala.scalajs.js.annotation._ @@ -34,9 +36,9 @@ private[java] object Utils { x.asInstanceOf[A] @inline - def undefOrGetOrElse[A](x: js.UndefOr[A])(default: => A): A = + def undefOrGetOrElse[A](x: js.UndefOr[A])(default: Supplier[A]): A = if (undefOrIsDefined(x)) undefOrForceGet(x) - else default + else default.get() @inline def undefOrGetOrNull[A >: Null](x: js.UndefOr[A]): A = @@ -44,15 +46,15 @@ private[java] object Utils { else null @inline - def undefOrForeach[A](x: js.UndefOr[A])(f: A => Any): Unit = { + def undefOrForeach[A](x: js.UndefOr[A])(f: Consumer[A]): Unit = { if (undefOrIsDefined(x)) - f(undefOrForceGet(x)) + f.accept(undefOrForceGet(x)) } @inline - def undefOrFold[A, B](x: js.UndefOr[A])(default: => B)(f: A => B): B = + def undefOrFold[A, B](x: js.UndefOr[A])(default: Supplier[B])(f: Function[A, B]): B = if (undefOrIsDefined(x)) f(undefOrForceGet(x)) - else default + else default.get() private object Cache { val safeHasOwnProperty = @@ -84,11 +86,11 @@ private[java] object Utils { @inline def dictGetOrElse[A](dict: js.Dictionary[_ <: A], key: String)( - default: => A): A = { + default: Supplier[A]): A = { if (dictContains(dict, key)) dictRawApply(dict, key) else - default + default.get() } def dictGetOrElseAndRemove[A](dict: js.Dictionary[_ <: A], key: String, @@ -139,27 +141,27 @@ private[java] object Utils { map.asInstanceOf[MapRaw[K, V]].set(key, value) @inline - def mapGetOrElse[K, V](map: js.Map[K, V], key: K)(default: => V): V = { + def mapGetOrElse[K, V](map: js.Map[K, V], key: K)(default: Supplier[V]): V = { val value = map.asInstanceOf[MapRaw[K, V]].getOrUndefined(key) if (!isUndefined(value) || mapHas(map, key)) value.asInstanceOf[V] - else default + else default.get() } @inline - def mapGetOrElseUpdate[K, V](map: js.Map[K, V], key: K)(default: => V): V = { - mapGetOrElse(map, key) { - val value = default + def mapGetOrElseUpdate[K, V](map: js.Map[K, V], key: K)(default: Supplier[V]): V = { + mapGetOrElse(map, key) { () => + val value = default.get() mapSet(map, key, value) value } } @inline - def forArrayElems[A](array: js.Array[A])(f: A => Any): Unit = { + def forArrayElems[A](array: js.Array[A])(f: Consumer[A]): Unit = { val len = array.length var i = 0 while (i != len) { - f(array(i)) + f.accept(array(i)) i += 1 } } @@ -173,12 +175,12 @@ private[java] object Utils { array.splice(index, 1)(0) @inline - def arrayExists[A](array: js.Array[A])(f: A => scala.Boolean): scala.Boolean = { + def arrayExists[A](array: js.Array[A])(f: Predicate[A]): scala.Boolean = { // scalastyle:off return val len = array.length var i = 0 while (i != len) { - if (f(array(i))) + if (f.test(array(i))) return true i += 1 } diff --git a/javalib/src/main/scala/java/lang/_String.scala b/javalib/src/main/scala/java/lang/_String.scala index d5b3e546fa..ea29540e37 100644 --- a/javalib/src/main/scala/java/lang/_String.scala +++ b/javalib/src/main/scala/java/lang/_String.scala @@ -19,13 +19,14 @@ import java.util.Comparator import scala.scalajs.js import scala.scalajs.js.annotation._ import scala.scalajs.js.JSStringOps.enableJSStringOps -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import scala.scalajs.LinkingInfo.ESVersion import java.lang.constant.{Constable, ConstantDesc} import java.nio.ByteBuffer import java.nio.charset.Charset import java.util.Locale +import java.util.function._ import java.util.regex._ /* This is the implementation of java.lang.String, which is a hijacked class. @@ -56,7 +57,7 @@ final class _String private () // scalastyle:ignore // Wasm intrinsic def codePointAt(index: Int): Int = { - if (linkingInfo.esVersion >= ESVersion.ES2015) { + if (LinkingInfo.esVersion >= ESVersion.ES2015) { charAt(index) // bounds check this.asInstanceOf[js.Dynamic].codePointAt(index).asInstanceOf[Int] } else { @@ -164,7 +165,7 @@ final class _String private () // scalastyle:ignore @inline def endsWith(suffix: String): scala.Boolean = { - if (linkingInfo.esVersion >= ESVersion.ES2015) { + if (LinkingInfo.esVersion >= ESVersion.ES2015) { suffix.getClass() // null check thisString.asInstanceOf[js.Dynamic].endsWith(suffix).asInstanceOf[scala.Boolean] } else { @@ -270,7 +271,7 @@ final class _String private () // scalastyle:ignore def repeat(count: Int): String = { if (count < 0) { throw new IllegalArgumentException - } else if (linkingInfo.esVersion >= ESVersion.ES2015) { + } else if (LinkingInfo.esVersion >= ESVersion.ES2015) { /* This will throw a `js.RangeError` if `count` is too large, instead of * an `OutOfMemoryError`. That's fine because the behavior of `repeat` is * not specified for `count` too large. @@ -316,7 +317,7 @@ final class _String private () // scalastyle:ignore @inline def startsWith(prefix: String): scala.Boolean = { - if (linkingInfo.esVersion >= ESVersion.ES2015) { + if (LinkingInfo.esVersion >= ESVersion.ES2015) { prefix.getClass() // null check thisString.asInstanceOf[js.Dynamic].startsWith(prefix).asInstanceOf[scala.Boolean] } else { @@ -326,7 +327,7 @@ final class _String private () // scalastyle:ignore @inline def startsWith(prefix: String, toffset: Int): scala.Boolean = { - if (linkingInfo.esVersion >= ESVersion.ES2015) { + if (LinkingInfo.esVersion >= ESVersion.ES2015) { prefix.getClass() // null check (toffset <= length() && toffset >= 0 && thisString.asInstanceOf[js.Dynamic].startsWith(prefix, toffset).asInstanceOf[scala.Boolean]) @@ -643,7 +644,7 @@ for (cp <- 0 to Character.MAX_CODE_POINT) { * the character at the given index is not special. */ @inline - private def replaceCharsAtIndex(replacementAtIndex: Int => String): String = { + private def replaceCharsAtIndex(replacementAtIndex: IntFunction[String]): String = { var prep = "" val len = this.length() var i = 0 @@ -776,7 +777,7 @@ for (cp <- 0 to Character.MAX_CODE_POINT) { def indent(n: Int): String = { - def forEachLn(f: String => String): String = { + def forEachLn(f: Function[String, String]): String = { var out = "" var i = 0 val xs = splitLines() diff --git a/javalib/src/main/scala/java/math/BigDecimal.scala b/javalib/src/main/scala/java/math/BigDecimal.scala index 98baffe296..d045ffc57e 100644 --- a/javalib/src/main/scala/java/math/BigDecimal.scala +++ b/javalib/src/main/scala/java/math/BigDecimal.scala @@ -1226,9 +1226,8 @@ class BigDecimal() extends Number with Comparable[BigDecimal] { def stripTrailingZeros(): BigDecimal = { if (isZero) { - // Preserve RI compatibility, so BigDecimal.equals (which checks - // value *and* scale) continues to work. - this + // As specified by the JavaDoc, we must return BigDecimal.ZERO, which has a scale of 0 + BigDecimal.ZERO } else { val lastPow = BigTenPows.length - 1 diff --git a/javalib/src/main/scala/java/math/BigInteger.scala b/javalib/src/main/scala/java/math/BigInteger.scala index dcae03d0f9..9864183573 100644 --- a/javalib/src/main/scala/java/math/BigInteger.scala +++ b/javalib/src/main/scala/java/math/BigInteger.scala @@ -45,6 +45,7 @@ import scala.annotation.tailrec import java.util.Random import java.util.ScalaOps._ +import java.util.function._ object BigInteger { @@ -124,12 +125,6 @@ object BigInteger { reference } - @inline - private def checkCriticalArgument(expression: Boolean, errorMessage: => String): Unit = { - if (!expression) - throw new IllegalArgumentException(errorMessage) - } - private[math] def checkRangeBasedOnIntArrayLength(byteLength: Int): Unit = { if (byteLength < 0 || byteLength >= ((Int.MaxValue + 1) >>> 5)) throw new ArithmeticException("BigInteger would overflow supported range") @@ -221,7 +216,10 @@ class BigInteger extends Number with Comparable[BigInteger] { def this(numBits: Int, rnd: Random) = { this() - checkCriticalArgument(numBits >= 0, "numBits must be non-negative") + + if (numBits < 0) + throw new IllegalArgumentException("numBits must be non-negative") + if (numBits == 0) { sign = 0 numberLength = 1 @@ -739,9 +737,9 @@ class BigInteger extends Number with Comparable[BigInteger] { @inline @tailrec - def loopBytes(tempDigit: Int => Unit): Unit = { + def loopBytes(tempDigit: IntConsumer): Unit = { if (bytesLen > firstByteNumber) { - tempDigit(digitIndex) + tempDigit.accept(digitIndex) loopBytes(tempDigit) } } diff --git a/javalib/src/main/scala/java/nio/GenBuffer.scala b/javalib/src/main/scala/java/nio/GenBuffer.scala index 9aa57e92d8..2fc5f52d3f 100644 --- a/javalib/src/main/scala/java/nio/GenBuffer.scala +++ b/javalib/src/main/scala/java/nio/GenBuffer.scala @@ -12,6 +12,7 @@ package java.nio +import java.util.function._ import java.util.internal.GenericArrayOps._ private[nio] object GenBuffer { @@ -137,7 +138,7 @@ private[nio] final class GenBuffer[B <: Buffer] private (val self: B) @inline def generic_compareTo(that: BufferType)( - compare: (ElementType, ElementType) => Int): Int = { + compare: BiFunction[ElementType, ElementType, Int]): Int = { // scalastyle:off return if (self eq that) { 0 diff --git a/javalib/src/main/scala/java/nio/charset/Charset.scala b/javalib/src/main/scala/java/nio/charset/Charset.scala index 752600eb8c..981bb16c07 100644 --- a/javalib/src/main/scala/java/nio/charset/Charset.scala +++ b/javalib/src/main/scala/java/nio/charset/Charset.scala @@ -80,7 +80,7 @@ object Charset { UTF_8 def forName(charsetName: String): Charset = { - dictGetOrElse(CharsetMap, charsetName.toLowerCase()) { + dictGetOrElse(CharsetMap, charsetName.toLowerCase()) { () => throw new UnsupportedCharsetException(charsetName) } } diff --git a/javalib/src/main/scala/java/nio/charset/CoderResult.scala b/javalib/src/main/scala/java/nio/charset/CoderResult.scala index 4cbb5a9fd0..1a4213f192 100644 --- a/javalib/src/main/scala/java/nio/charset/CoderResult.scala +++ b/javalib/src/main/scala/java/nio/charset/CoderResult.scala @@ -78,7 +78,7 @@ object CoderResult { } private def malformedForLengthImpl(length: Int): CoderResult = { - undefOrFold(uniqueMalformed(length)) { + undefOrFold(uniqueMalformed(length)) { () => val result = new CoderResult(Malformed, length) uniqueMalformed(length) = result result @@ -96,7 +96,7 @@ object CoderResult { } private def unmappableForLengthImpl(length: Int): CoderResult = { - undefOrFold(uniqueUnmappable(length)) { + undefOrFold(uniqueUnmappable(length)) { () => val result = new CoderResult(Unmappable, length) uniqueUnmappable(length) = result result diff --git a/javalib/src/main/scala/java/util/AbstractCollection.scala b/javalib/src/main/scala/java/util/AbstractCollection.scala index 8385d981da..34920f1bdb 100644 --- a/javalib/src/main/scala/java/util/AbstractCollection.scala +++ b/javalib/src/main/scala/java/util/AbstractCollection.scala @@ -18,6 +18,8 @@ import ScalaOps._ import java.lang.{reflect => jlr} +import java.util.function.Predicate + abstract class AbstractCollection[E] protected () extends Collection[E] { def iterator(): Iterator[E] def size(): Int @@ -78,11 +80,11 @@ abstract class AbstractCollection[E] protected () extends Collection[E] { def clear(): Unit = removeWhere(_ => true) - private def removeWhere(p: Any => Boolean): Boolean = { + private def removeWhere(p: Predicate[Any]): Boolean = { val iter = iterator() var changed = false while (iter.hasNext()) { - if (p(iter.next())) { + if (p.test(iter.next())) { iter.remove() changed = true } diff --git a/javalib/src/main/scala/java/util/AbstractMap.scala b/javalib/src/main/scala/java/util/AbstractMap.scala index df75f5ba67..00249d5911 100644 --- a/javalib/src/main/scala/java/util/AbstractMap.scala +++ b/javalib/src/main/scala/java/util/AbstractMap.scala @@ -94,7 +94,7 @@ abstract class AbstractMap[K, V] protected () extends java.util.Map[K, V] { entrySet().scalaOps.exists(entry => Objects.equals(key, entry.getKey())) def get(key: Any): V = { - entrySet().scalaOps.findFold(entry => Objects.equals(key, entry.getKey())) { + entrySet().scalaOps.findFold(entry => Objects.equals(key, entry.getKey())) { () => null.asInstanceOf[V] } { entry => entry.getValue() diff --git a/javalib/src/main/scala/java/util/ArrayList.scala b/javalib/src/main/scala/java/util/ArrayList.scala index 68b9705f62..1c67de682b 100644 --- a/javalib/src/main/scala/java/util/ArrayList.scala +++ b/javalib/src/main/scala/java/util/ArrayList.scala @@ -14,80 +14,181 @@ package java.util import java.lang.Cloneable import java.lang.Utils._ +import java.util.ScalaOps._ import scala.scalajs._ +import scala.scalajs.LinkingInfo.isWebAssembly -class ArrayList[E] private (private[ArrayList] val inner: js.Array[E]) +class ArrayList[E] private (innerInit: AnyRef, private var _size: Int) extends AbstractList[E] with RandomAccess with Cloneable with Serializable { self => + /* This class has two different implementations for handling the + * internal data storage, depending on whether we are on Wasm or JS. + * On JS, we utilize `js.Array`. On Wasm, for performance reasons, + * we avoid JS interop and use a scala.Array. + * The `_size` field (unused in JS) keeps track of the effective size + * of the underlying Array for the Wasm implementation. + */ + + private val innerJS: js.Array[E] = + if (isWebAssembly) null + else innerInit.asInstanceOf[js.Array[E]] + + private var innerWasm: Array[AnyRef] = + if (!isWebAssembly) null + else innerInit.asInstanceOf[Array[AnyRef]] + def this(initialCapacity: Int) = { - this(new js.Array[E]) - if (initialCapacity < 0) - throw new IllegalArgumentException + this( + { + if (initialCapacity < 0) + throw new IllegalArgumentException + if (isWebAssembly) new Array[AnyRef](initialCapacity) + else new js.Array[E] + }, + 0 + ) } - def this() = - this(new js.Array[E]) + def this() = this(16) def this(c: Collection[_ <: E]) = { - this() + this(c.size()) addAll(c) } def trimToSize(): Unit = { - // We ignore this as js.Array doesn't support explicit pre-allocation + if (isWebAssembly) + resizeTo(size()) + // We ignore this in JS as js.Array doesn't support explicit pre-allocation } def ensureCapacity(minCapacity: Int): Unit = { - // We ignore this as js.Array doesn't support explicit pre-allocation + if (isWebAssembly) { + if (innerWasm.length < minCapacity) { + if (minCapacity > (1 << 30)) + resizeTo(minCapacity) + else + resizeTo(((1 << 31) >>> (Integer.numberOfLeadingZeros(minCapacity - 1)) - 1)) + } + } + // We ignore this in JS as js.Array doesn't support explicit pre-allocation } def size(): Int = - inner.length - - override def clone(): AnyRef = - new ArrayList(inner.jsSlice(0)) + if (isWebAssembly) _size + else innerJS.length + + override def clone(): AnyRef = { + if (isWebAssembly) + new ArrayList(innerWasm.clone(), size()) + else + new ArrayList(innerJS.jsSlice(0), 0) + } def get(index: Int): E = { checkIndexInBounds(index) - inner(index) + if (isWebAssembly) + innerWasm(index).asInstanceOf[E] + else + innerJS(index) } override def set(index: Int, element: E): E = { val e = get(index) - inner(index) = element + if (isWebAssembly) + innerWasm(index) = element.asInstanceOf[AnyRef] + else + innerJS(index) = element e } override def add(e: E): Boolean = { - inner.push(e) + if (isWebAssembly) { + if (size() >= innerWasm.length) + expand() + innerWasm(size()) = e.asInstanceOf[AnyRef] + _size += 1 + } else { + innerJS.push(e) + } true } override def add(index: Int, element: E): Unit = { checkIndexOnBounds(index) - inner.splice(index, 0, element) + if (isWebAssembly) { + if (size() >= innerWasm.length) + expand() + System.arraycopy(innerWasm, index, innerWasm, index + 1, size() - index) + innerWasm(index) = element.asInstanceOf[AnyRef] + _size += 1 + } else { + innerJS.splice(index, 0, element) + } } override def remove(index: Int): E = { checkIndexInBounds(index) - arrayRemoveAndGet(inner, index) + if (isWebAssembly) { + val removed = innerWasm(index).asInstanceOf[E] + System.arraycopy(innerWasm, index + 1, innerWasm, index, size() - index - 1) + innerWasm(size - 1) = null // free reference for GC + _size -= 1 + removed + } else { + arrayRemoveAndGet(innerJS, index) + } } override def clear(): Unit = - inner.length = 0 + if (isWebAssembly) { + Arrays.fill(innerWasm, null) // free references for GC + _size = 0 + } else { + innerJS.length = 0 + } override def addAll(index: Int, c: Collection[_ <: E]): Boolean = { c match { case other: ArrayList[_] => - inner.splice(index, 0, other.inner.toSeq: _*) + checkIndexOnBounds(index) + if (isWebAssembly) { + ensureCapacity(size() + other.size()) + System.arraycopy(innerWasm, index, innerWasm, index + other.size(), size() - index) + System.arraycopy(other.innerWasm, 0, innerWasm, index, other.size()) + _size += c.size() + } else { + innerJS.splice(index, 0, other.innerJS.toSeq: _*) + } other.size() > 0 case _ => super.addAll(index, c) } } - override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = - inner.splice(fromIndex, toIndex - fromIndex) + override protected def removeRange(fromIndex: Int, toIndex: Int): Unit = { + if (fromIndex < 0 || toIndex > size() || toIndex < fromIndex) + throw new IndexOutOfBoundsException() + if (isWebAssembly) { + if (fromIndex != toIndex) { + System.arraycopy(innerWasm, toIndex, innerWasm, fromIndex, size() - toIndex) + val newSize = size() - toIndex + fromIndex + Arrays.fill(innerWasm, newSize, size(), null) // free references for GC + _size = newSize + } + } else { + innerJS.splice(fromIndex, toIndex - fromIndex) + } + } + // Wasm only + private def expand(): Unit = { + resizeTo(Math.max(innerWasm.length * 2, 16)) + } + + // Wasm only + private def resizeTo(newCapacity: Int): Unit = { + innerWasm = Arrays.copyOf(innerWasm, newCapacity) + } } diff --git a/javalib/src/main/scala/java/util/Collections.scala b/javalib/src/main/scala/java/util/Collections.scala index 687ba74b5e..8f9a630de5 100644 --- a/javalib/src/main/scala/java/util/Collections.scala +++ b/javalib/src/main/scala/java/util/Collections.scala @@ -14,6 +14,7 @@ package java.util import java.{lang => jl} import java.io.Serializable +import java.util.function._ import scala.language.implicitConversions @@ -79,16 +80,16 @@ object Collections { binarySearchImpl(list, (elem: T) => c.compare(elem, key)) @inline - private def binarySearchImpl[E](list: List[E], compareToKey: E => Int): Int = { + private def binarySearchImpl[E](list: List[_ <: E], compareToKey: ToIntFunction[E]): Int = { def notFound(insertionPoint: Int): Int = { -insertionPoint - 1 } @tailrec - def binarySearch(lo: Int, hi: Int, get: Int => E): Int = { + def binarySearch(lo: Int, hi: Int, get: IntFunction[E]): Int = { if (lo < hi) { val mid = lo + (hi - lo) / 2 - val cmp = compareToKey(get(mid)) + val cmp = compareToKey.applyAsInt(get(mid)) if (cmp == 0) mid else if (cmp > 0) binarySearch(lo, mid, get) else binarySearch(mid + 1, hi, get) @@ -102,7 +103,7 @@ object Collections { binarySearch(0, list.size(), list.get(_)) case _ => - def getFrom(iter: ListIterator[E])(index: Int): E = { + def getFrom(iter: ListIterator[_ <: E])(index: Int): E = { val shift = index - iter.nextIndex() if (shift > 0) (0 until shift).foreach(_ => iter.next()) @@ -263,14 +264,14 @@ object Collections { min(coll, Comparator.naturalOrder[T]) def min[T](coll: Collection[_ <: T], comp: Comparator[_ >: T]): T = - coll.scalaOps.reduceLeft((a, b) => if (comp.compare(a, b) <= 0) a else b) + coll.scalaOps.reduceLeft[T]((a, b) => if (comp.compare(a, b) <= 0) a else b) // Differs from original type definition, original: [T <: jl.Comparable[_ >: T]], returning T def max[T <: AnyRef with jl.Comparable[T]](coll: Collection[_ <: T]): AnyRef = max(coll, Comparator.naturalOrder[T]) def max[T](coll: Collection[_ <: T], comp: Comparator[_ >: T]): T = - coll.scalaOps.reduceLeft((a, b) => if (comp.compare(a, b) >= 0) a else b) + coll.scalaOps.reduceLeft[T]((a, b) => if (comp.compare(a, b) >= 0) a else b) def rotate(list: List[_], distance: Int): Unit = rotateImpl(list, distance) diff --git a/javalib/src/main/scala/java/util/Date.scala b/javalib/src/main/scala/java/util/Date.scala index 170d5829d2..68fe483627 100644 --- a/javalib/src/main/scala/java/util/Date.scala +++ b/javalib/src/main/scala/java/util/Date.scala @@ -14,6 +14,7 @@ package java.util import java.lang.Cloneable import java.time.Instant +import java.util.function._ import scalajs.js @@ -69,9 +70,9 @@ class Date(private var millis: Long) extends Object } @inline - private def mutDate(mutator: js.Date => Unit): Unit = { + private def mutDate(mutator: Consumer[js.Date]): Unit = { val date = asDate() - mutator(date) + mutator.accept(date) millis = safeGetTime(date) } diff --git a/javalib/src/main/scala/java/util/Deque.scala b/javalib/src/main/scala/java/util/Deque.scala index 89b70bc615..d4a4e0918c 100644 --- a/javalib/src/main/scala/java/util/Deque.scala +++ b/javalib/src/main/scala/java/util/Deque.scala @@ -12,7 +12,7 @@ package java.util -trait Deque[E] extends Queue[E] { +trait Deque[E] extends Queue[E] with SequencedCollection[E] { def addFirst(e: E): Unit def addLast(e: E): Unit def offerFirst(e: E): Boolean diff --git a/javalib/src/main/scala/java/util/Formatter.scala b/javalib/src/main/scala/java/util/Formatter.scala index 750bebf4c4..909fab1929 100644 --- a/javalib/src/main/scala/java/util/Formatter.scala +++ b/javalib/src/main/scala/java/util/Formatter.scala @@ -51,9 +51,9 @@ final class Formatter private (private[this] var dest: Appendable, def this(a: Appendable, l: Locale) = this(a, new Formatter.LocaleLocaleInfo(l)) @inline - private def trapIOExceptions(body: => Unit): Unit = { + private def trapIOExceptions(body: Runnable): Unit = { try { - body + body.run() } catch { case th: IOException => lastIOException = th @@ -83,7 +83,7 @@ final class Formatter private (private[this] var dest: Appendable, @noinline private def sendToDestSlowPath(ss: js.Array[String]): Unit = { - trapIOExceptions { + trapIOExceptions { () => forArrayElems(ss)(dest.append(_)) } } @@ -92,7 +92,7 @@ final class Formatter private (private[this] var dest: Appendable, if (!closed && (dest ne null)) { dest match { case cl: Closeable => - trapIOExceptions { + trapIOExceptions { () => cl.close() } case _ => @@ -106,7 +106,7 @@ final class Formatter private (private[this] var dest: Appendable, if (dest ne null) { dest match { case fl: Flushable => - trapIOExceptions { + trapIOExceptions { () => fl.flush() } case _ => @@ -335,7 +335,7 @@ final class Formatter private (private[this] var dest: Appendable, * Int range. */ private def parsePositiveInt(capture: js.UndefOr[String]): Int = { - undefOrFold(capture) { + undefOrFold(capture) { () => -1 } { s => val x = js.Dynamic.global.parseInt(s, 10).asInstanceOf[Double] @@ -993,7 +993,7 @@ object Formatter { } @inline - private def assert(condition: Boolean, msg: => String): Unit = { + private def assert(condition: Boolean, msg: String): Unit = { if (!condition) throw new AssertionError(msg) } diff --git a/javalib/src/main/scala/java/util/LinkedHashMap.scala b/javalib/src/main/scala/java/util/LinkedHashMap.scala index ba46ffc844..958aeff409 100644 --- a/javalib/src/main/scala/java/util/LinkedHashMap.scala +++ b/javalib/src/main/scala/java/util/LinkedHashMap.scala @@ -17,7 +17,7 @@ import java.util.function.BiConsumer class LinkedHashMap[K, V](initialCapacity: Int, loadFactor: Float, accessOrder: Boolean) - extends HashMap[K, V](initialCapacity, loadFactor) { + extends HashMap[K, V](initialCapacity, loadFactor) with SequencedMap[K, V] { self => import LinkedHashMap._ diff --git a/javalib/src/main/scala/java/util/LinkedHashSet.scala b/javalib/src/main/scala/java/util/LinkedHashSet.scala index 6133d013d9..f24d3f5bd4 100644 --- a/javalib/src/main/scala/java/util/LinkedHashSet.scala +++ b/javalib/src/main/scala/java/util/LinkedHashSet.scala @@ -15,7 +15,7 @@ package java.util import java.lang.Cloneable class LinkedHashSet[E] private[util] (inner: LinkedHashMap[E, Any]) - extends HashSet[E](inner) with Set[E] with Cloneable with Serializable { + extends HashSet[E](inner) with SequencedSet[E] with Cloneable with Serializable { def this(initialCapacity: Int, loadFactor: Float) = this(new LinkedHashMap[E, Any](initialCapacity, loadFactor)) diff --git a/javalib/src/main/scala/java/util/List.scala b/javalib/src/main/scala/java/util/List.scala index 66465dde50..982e373b01 100644 --- a/javalib/src/main/scala/java/util/List.scala +++ b/javalib/src/main/scala/java/util/List.scala @@ -14,7 +14,7 @@ package java.util import java.util.function.UnaryOperator -trait List[E] extends Collection[E] { +trait List[E] extends SequencedCollection[E] { def replaceAll(operator: UnaryOperator[E]): Unit = { val iter = listIterator() while (iter.hasNext()) diff --git a/javalib/src/main/scala/java/util/Properties.scala b/javalib/src/main/scala/java/util/Properties.scala index d75d89e601..d70e639fa4 100644 --- a/javalib/src/main/scala/java/util/Properties.scala +++ b/javalib/src/main/scala/java/util/Properties.scala @@ -19,6 +19,7 @@ import java.{lang => jl} import java.{util => ju} import java.io._ import java.nio.charset.StandardCharsets +import java.util.function._ import scala.scalajs.js @@ -114,8 +115,8 @@ class Properties(protected val defaults: Properties) } @inline @tailrec - private final def foreachAncestor(f: Properties => Unit): Unit = { - f(this) + private final def foreachAncestor(f: Consumer[Properties]): Unit = { + f.accept(this) if (defaults ne null) defaults.foreachAncestor(f) } diff --git a/javalib/src/main/scala/java/util/Random.scala b/javalib/src/main/scala/java/util/Random.scala index 8cd654cd1f..7840b8c3c1 100644 --- a/javalib/src/main/scala/java/util/Random.scala +++ b/javalib/src/main/scala/java/util/Random.scala @@ -15,9 +15,13 @@ package java.util import scala.annotation.tailrec import scala.scalajs.js -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo + +import java.util.random.RandomGenerator + +class Random(seed_in: Long) + extends AnyRef with RandomGenerator with java.io.Serializable { -class Random(seed_in: Long) extends AnyRef with java.io.Serializable { /* This class has two different implementations of seeding and computing * bits, depending on whether we are on Wasm or JS. On Wasm, we use the * implementation specified in the JavaDoc verbatim. On JS, however, that is @@ -39,7 +43,7 @@ class Random(seed_in: Long) extends AnyRef with java.io.Serializable { def setSeed(seed_in: Long): Unit = { val seed = ((seed_in ^ 0x5DEECE66DL) & ((1L << 48) - 1)) // as documented - if (linkingInfo.isWebAssembly) { + if (LinkingInfo.isWebAssembly) { this.seed = seed } else { seedHi = (seed >>> 24).toInt @@ -50,7 +54,7 @@ class Random(seed_in: Long) extends AnyRef with java.io.Serializable { @noinline protected def next(bits: Int): Int = - if (linkingInfo.isWebAssembly) nextWasm(bits) + if (LinkingInfo.isWebAssembly) nextWasm(bits) else nextJS(bits) @inline @@ -108,16 +112,16 @@ class Random(seed_in: Long) extends AnyRef with java.io.Serializable { result32 >>> (32 - bits) } - def nextDouble(): Double = { + override def nextDouble(): Double = { // ((next(26).toLong << 27) + next(27)) / (1L << 53).toDouble ((next(26).toDouble * (1L << 27).toDouble) + next(27).toDouble) / (1L << 53).toDouble } - def nextBoolean(): Boolean = next(1) != 0 + override def nextBoolean(): Boolean = next(1) != 0 - def nextInt(): Int = next(32) + override def nextInt(): Int = next(32) - def nextInt(n: Int): Int = { + override def nextInt(n: Int): Int = { if (n <= 0) { throw new IllegalArgumentException("n must be positive") } else if ((n & -n) == n) { // i.e., n is a power of 2 @@ -148,12 +152,12 @@ class Random(seed_in: Long) extends AnyRef with java.io.Serializable { def nextLong(): Long = (next(32).toLong << 32) + next(32) - def nextFloat(): Float = { + override def nextFloat(): Float = { // next(24).toFloat / (1 << 24).toFloat (next(24).toDouble / (1 << 24).toDouble).toFloat } - def nextBytes(bytes: Array[Byte]): Unit = { + override def nextBytes(bytes: Array[Byte]): Unit = { var i = 0 while (i < bytes.length) { var rnd = nextInt() diff --git a/javalib/src/main/scala/java/util/RedBlackTree.scala b/javalib/src/main/scala/java/util/RedBlackTree.scala index 3e91b22e66..a1554e264d 100644 --- a/javalib/src/main/scala/java/util/RedBlackTree.scala +++ b/javalib/src/main/scala/java/util/RedBlackTree.scala @@ -14,6 +14,8 @@ package java.util import scala.annotation.tailrec +import java.util.function._ + import scala.scalajs.js /** The red-black tree implementation used by `TreeSet`s and `TreeMap`s. @@ -609,7 +611,7 @@ private[util] object RedBlackTree { /** Returns `null.asInstanceOf[C]` if `node eq null`, otherwise `f(node)`. */ @inline - private def nullableNodeFlatMap[A, B, C](node: Node[A, B])(f: Node[A, B] => C): C = + private def nullableNodeFlatMap[A, B, C](node: Node[A, B])(f: Function[Node[A, B], C]): C = if (node eq null) null.asInstanceOf[C] else f(node) diff --git a/javalib/src/main/scala/java/util/ScalaOps.scala b/javalib/src/main/scala/java/util/ScalaOps.scala index 32023f2ab7..a00c7d379f 100644 --- a/javalib/src/main/scala/java/util/ScalaOps.scala +++ b/javalib/src/main/scala/java/util/ScalaOps.scala @@ -12,6 +12,8 @@ package java.util +import java.util.function._ + /** Make some Scala collection APIs available on Java collections. */ private[java] object ScalaOps { @@ -26,10 +28,10 @@ private[java] object ScalaOps { @inline final class SimpleRange(start: Int, end: Int) { @inline - def foreach[U](f: Int => U): Unit = { + def foreach[U](f: IntConsumer): Unit = { var i = start while (i < end) { - f(i) + f.accept(i) i += 1 } } @@ -38,10 +40,10 @@ private[java] object ScalaOps { @inline final class SimpleInclusiveRange(start: Int, end: Int) { @inline - def foreach[U](f: Int => U): Unit = { + def foreach[U](f: IntConsumer): Unit = { var i = start while (i <= end) { - f(i) + f.accept(i) i += 1 } } @@ -57,28 +59,28 @@ private[java] object ScalaOps { val __self: java.lang.Iterable[A]) extends AnyVal { - @inline def foreach[U](f: A => U): Unit = + @inline def foreach(f: Consumer[A]): Unit = __self.iterator().scalaOps.foreach(f) - @inline def count(f: A => Boolean): Int = + @inline def count(f: Predicate[A]): Int = __self.iterator().scalaOps.count(f) - @inline def exists(f: A => Boolean): Boolean = + @inline def exists(f: Predicate[A]): Boolean = __self.iterator().scalaOps.exists(f) - @inline def forall(f: A => Boolean): Boolean = + @inline def forall(f: Predicate[A]): Boolean = __self.iterator().scalaOps.forall(f) - @inline def indexWhere(f: A => Boolean): Int = + @inline def indexWhere(f: Predicate[A]): Int = __self.iterator().scalaOps.indexWhere(f) - @inline def findFold[B](f: A => Boolean)(default: => B)(g: A => B): B = + @inline def findFold[B](f: Predicate[A])(default: Supplier[B])(g: Function[A, B]): B = __self.iterator().scalaOps.findFold(f)(default)(g) - @inline def foldLeft[B](z: B)(f: (B, A) => B): B = + @inline def foldLeft[B](z: B)(f: BiFunction[B, A, B]): B = __self.iterator().scalaOps.foldLeft(z)(f) - @inline def reduceLeft[B >: A](f: (B, A) => B): B = + @inline def reduceLeft[B >: A](f: BiFunction[B, A, B]): B = __self.iterator().scalaOps.reduceLeft(f) @inline def mkString(start: String, sep: String, end: String): String = @@ -94,32 +96,32 @@ private[java] object ScalaOps { class JavaIteratorOps[A] private[ScalaOps] (val __self: Iterator[A]) extends AnyVal { - @inline def foreach[U](f: A => U): Unit = { + @inline def foreach(f: Consumer[A]): Unit = { while (__self.hasNext()) - f(__self.next()) + f.accept(__self.next()) } - @inline def count(f: A => Boolean): Int = - foldLeft(0)((prev, x) => if (f(x)) prev + 1 else prev) + @inline def count(f: Predicate[A]): Int = + foldLeft(0)((prev, x) => if (f.test(x)) prev + 1 else prev) - @inline def exists(f: A => Boolean): Boolean = { + @inline def exists(f: Predicate[A]): Boolean = { // scalastyle:off return while (__self.hasNext()) { - if (f(__self.next())) + if (f.test(__self.next())) return true } false // scalastyle:on return } - @inline def forall(f: A => Boolean): Boolean = - !exists(x => !f(x)) + @inline def forall(f: Predicate[A]): Boolean = + !exists(x => !f.test(x)) - @inline def indexWhere(f: A => Boolean): Int = { + @inline def indexWhere(f: Predicate[A]): Int = { // scalastyle:off return var i = 0 while (__self.hasNext()) { - if (f(__self.next())) + if (f.test(__self.next())) return i i += 1 } @@ -127,25 +129,25 @@ private[java] object ScalaOps { // scalastyle:on return } - @inline def findFold[B](f: A => Boolean)(default: => B)(g: A => B): B = { + @inline def findFold[B](f: Predicate[A])(default: Supplier[B])(g: Function[A, B]): B = { // scalastyle:off return while (__self.hasNext()) { val x = __self.next() - if (f(x)) + if (f.test(x)) return g(x) } - default + default.get() // scalastyle:on return } - @inline def foldLeft[B](z: B)(f: (B, A) => B): B = { + @inline def foldLeft[B](z: B)(f: BiFunction[B, A, B]): B = { var result: B = z while (__self.hasNext()) result = f(result, __self.next()) result } - @inline def reduceLeft[B >: A](f: (B, A) => B): B = { + @inline def reduceLeft[B >: A](f: BiFunction[B, A, B]): B = { if (!__self.hasNext()) throw new NoSuchElementException("collection is empty") foldLeft[B](__self.next())(f) @@ -174,9 +176,9 @@ private[java] object ScalaOps { class JavaEnumerationOps[A] private[ScalaOps] (val __self: Enumeration[A]) extends AnyVal { - @inline def foreach[U](f: A => U): Unit = { + @inline def foreach(f: Consumer[A]): Unit = { while (__self.hasMoreElements()) - f(__self.nextElement()) + f.accept(__self.nextElement()) } } diff --git a/javalib/src/main/scala/java/util/SequencedCollection.scala b/javalib/src/main/scala/java/util/SequencedCollection.scala new file mode 100644 index 0000000000..9f49537f33 --- /dev/null +++ b/javalib/src/main/scala/java/util/SequencedCollection.scala @@ -0,0 +1,15 @@ +/* + * 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 java.util + +trait SequencedCollection[E] extends Collection[E] diff --git a/javalib/src/main/scala/java/util/SequencedMap.scala b/javalib/src/main/scala/java/util/SequencedMap.scala new file mode 100644 index 0000000000..a1eb13cd23 --- /dev/null +++ b/javalib/src/main/scala/java/util/SequencedMap.scala @@ -0,0 +1,17 @@ +/* + * 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 java.util + +import java.util.Map.Entry + +trait SequencedMap[K, V] extends Map[K, V] diff --git a/javalib/src/main/scala/java/util/SequencedSet.scala b/javalib/src/main/scala/java/util/SequencedSet.scala new file mode 100644 index 0000000000..8bf692997a --- /dev/null +++ b/javalib/src/main/scala/java/util/SequencedSet.scala @@ -0,0 +1,15 @@ +/* + * 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 java.util + +trait SequencedSet[E] extends SequencedCollection[E] with Set[E] diff --git a/javalib/src/main/scala/java/util/SortedMap.scala b/javalib/src/main/scala/java/util/SortedMap.scala index e2ac54cfef..c5076c0019 100644 --- a/javalib/src/main/scala/java/util/SortedMap.scala +++ b/javalib/src/main/scala/java/util/SortedMap.scala @@ -12,7 +12,7 @@ package java.util -trait SortedMap[K, V] extends Map[K, V] { +trait SortedMap[K, V] extends SequencedMap[K, V] { def firstKey(): K def comparator(): Comparator[_ >: K] def lastKey(): K diff --git a/javalib/src/main/scala/java/util/SortedSet.scala b/javalib/src/main/scala/java/util/SortedSet.scala index b4f6fcc83d..95694ab938 100644 --- a/javalib/src/main/scala/java/util/SortedSet.scala +++ b/javalib/src/main/scala/java/util/SortedSet.scala @@ -12,7 +12,7 @@ package java.util -trait SortedSet[E] extends Set[E] { +trait SortedSet[E] extends SequencedSet[E] { def comparator(): Comparator[_ >: E] def subSet(fromElement: E, toElement: E): SortedSet[E] def headSet(toElement: E): SortedSet[E] diff --git a/javalib/src/main/scala/java/util/SplittableRandom.scala b/javalib/src/main/scala/java/util/SplittableRandom.scala index 9d394b909c..8fced3e262 100644 --- a/javalib/src/main/scala/java/util/SplittableRandom.scala +++ b/javalib/src/main/scala/java/util/SplittableRandom.scala @@ -12,6 +12,8 @@ package java.util +import java.util.random.RandomGenerator + /* * This is a clean room implementation derived from the original paper * and Java implementation mentioned there: @@ -23,7 +25,6 @@ package java.util */ private object SplittableRandom { - private final val DoubleULP = 1.0 / (1L << 53) private final val GoldenGamma = 0x9e3779b97f4a7c15L private var defaultGen: Long = new Random().nextLong() @@ -80,7 +81,8 @@ private object SplittableRandom { } -final class SplittableRandom private (private var seed: Long, gamma: Long) { +final class SplittableRandom private (private var seed: Long, gamma: Long) + extends RandomGenerator { import SplittableRandom._ def this(seed: Long) = { @@ -106,27 +108,13 @@ final class SplittableRandom private (private var seed: Long, gamma: Long) { seed } - def nextInt(): Int = mix32(nextSeed()) - - //def nextInt(bound: Int): Int - - //def nextInt(origin: Int, bound: Int): Int + /* According to the JavaDoc, this method is not overridden anymore. + * However, if we remove our override, we break tests in + * `SplittableRandomTest`. I don't know how the JDK produces the values it + * produces without that override. So we keep it on our side. + */ + override def nextInt(): Int = mix32(nextSeed()) def nextLong(): Long = mix64(nextSeed()) - //def nextLong(bound: Long): Long - - //def nextLong(origin: Long, bound: Long): Long - - def nextDouble(): Double = - (nextLong() >>> 11).toDouble * DoubleULP - - //def nextDouble(bound: Double): Double - - //def nextDouble(origin: Double, bound: Double): Double - - // this should be properly tested - // looks to work but just by chance maybe - def nextBoolean(): Boolean = nextInt() < 0 - } diff --git a/javalib/src/main/scala/java/util/Timer.scala b/javalib/src/main/scala/java/util/Timer.scala index b377b5a33f..e802db4a31 100644 --- a/javalib/src/main/scala/java/util/Timer.scala +++ b/javalib/src/main/scala/java/util/Timer.scala @@ -47,7 +47,7 @@ class Timer() { private def scheduleOnce(task: TimerTask, delay: Long): Unit = { acquire(task) - task.timeout(delay) { + task.timeout(delay) { () => task.scheduledOnceAndStarted = true task.doRun() } @@ -76,12 +76,12 @@ class Timer() { task.doRun() val endTime = System.nanoTime() val duration = (endTime - startTime) / 1000000 - task.timeout(period - duration) { + task.timeout(period - duration) { () => loop() } } - task.timeout(delay) { + task.timeout(delay) { () => loop() } } @@ -112,13 +112,13 @@ class Timer() { loop(nextScheduledTime) } else { // Re-run after a timeout. - task.timeout(nextScheduledTime - nowTime) { + task.timeout(nextScheduledTime - nowTime) { () => loop(nextScheduledTime) } } } - task.timeout(delay) { + task.timeout(delay) { () => loop(System.nanoTime() / 1000000L + period) } } diff --git a/javalib/src/main/scala/java/util/TimerTask.scala b/javalib/src/main/scala/java/util/TimerTask.scala index 4299157e89..439add8f55 100644 --- a/javalib/src/main/scala/java/util/TimerTask.scala +++ b/javalib/src/main/scala/java/util/TimerTask.scala @@ -41,9 +41,9 @@ abstract class TimerTask { def scheduledExecutionTime(): Long = lastScheduled - private[util] def timeout(delay: Long)(body: => Unit): Unit = { + private[util] def timeout(delay: Long)(body: js.Function0[Any]): Unit = { if (!canceled) { - handle = setTimeout(() => body, delay.toDouble) + handle = setTimeout(body, delay.toDouble) } } diff --git a/javalib/src/main/scala/java/util/concurrent/ThreadLocalRandom.scala b/javalib/src/main/scala/java/util/concurrent/ThreadLocalRandom.scala index ce6ab96c6e..ed5ce571a1 100644 --- a/javalib/src/main/scala/java/util/concurrent/ThreadLocalRandom.scala +++ b/javalib/src/main/scala/java/util/concurrent/ThreadLocalRandom.scala @@ -9,7 +9,6 @@ package java.util.concurrent import java.util.Random -import scala.annotation.tailrec class ThreadLocalRandom extends Random { @@ -22,98 +21,6 @@ class ThreadLocalRandom extends Random { super.setSeed(seed) } - - def nextInt(least: Int, bound: Int): Int = { - if (least >= bound) - throw new IllegalArgumentException() - - val difference = bound - least - if (difference > 0) { - nextInt(difference) + least - } else { - /* The interval size here is greater than Int.MaxValue, - * so the loop will exit with a probability of at least 1/2. - */ - @tailrec - def loop(): Int = { - val n = nextInt() - if (n >= least && n < bound) n - else loop() - } - - loop() - } - } - - def nextLong(_n: Long): Long = { - if (_n <= 0) - throw new IllegalArgumentException("n must be positive") - - /* - * Divide n by two until small enough for nextInt. On each - * iteration (at most 31 of them but usually much less), - * randomly choose both whether to include high bit in result - * (offset) and whether to continue with the lower vs upper - * half (which makes a difference only if odd). - */ - - var offset = 0L - var n = _n - - while (n >= Integer.MAX_VALUE) { - val bits = next(2) - val halfn = n >>> 1 - val nextn = - if ((bits & 2) == 0) halfn - else n - halfn - if ((bits & 1) == 0) - offset += n - nextn - n = nextn - } - offset + nextInt(n.toInt) - } - - def nextLong(least: Long, bound: Long): Long = { - if (least >= bound) - throw new IllegalArgumentException() - - val difference = bound - least - if (difference > 0) { - nextLong(difference) + least - } else { - /* The interval size here is greater than Long.MaxValue, - * so the loop will exit with a probability of at least 1/2. - */ - @tailrec - def loop(): Long = { - val n = nextLong() - if (n >= least && n < bound) n - else loop() - } - - loop() - } - } - - def nextDouble(n: Double): Double = { - if (n <= 0) - throw new IllegalArgumentException("n must be positive") - - nextDouble() * n - } - - def nextDouble(least: Double, bound: Double): Double = { - if (least >= bound) - throw new IllegalArgumentException() - - /* Based on documentation for Random.doubles to avoid issue #2144 and other - * possible rounding up issues: - * https://docs.oracle.com/javase/8/docs/api/java/util/Random.html#doubles-double-double- - */ - val next = nextDouble() * (bound - least) + least - if (next < bound) next - else Math.nextAfter(bound, Double.NegativeInfinity) - } } object ThreadLocalRandom { diff --git a/javalib/src/main/scala/java/util/random/RandomGenerator.scala b/javalib/src/main/scala/java/util/random/RandomGenerator.scala new file mode 100644 index 0000000000..ddb38b0469 --- /dev/null +++ b/javalib/src/main/scala/java/util/random/RandomGenerator.scala @@ -0,0 +1,335 @@ +/* + * 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 java.util.random + +import scala.annotation.tailrec + +import java.util.ScalaOps._ + +trait RandomGenerator { + // Comments starting with `// >` are cited from the JavaDoc. + + // Not implemented: all the methods using Streams + + // Not implemented, because + // > The default implementation checks for the @Deprecated annotation. + // def isDeprecated(): Boolean = ??? + + def nextBoolean(): Boolean = + nextInt() < 0 // is the sign bit 1? + + def nextBytes(bytes: Array[Byte]): Unit = { + val len = bytes.length // implicit NPE + var i = 0 + + for (_ <- 0 until (len >> 3)) { + var rnd = nextLong() + for (_ <- 0 until 8) { + bytes(i) = rnd.toByte + rnd >>>= 8 + i += 1 + } + } + + if (i != len) { + var rnd = nextLong() + while (i != len) { + bytes(i) = rnd.toByte + rnd >>>= 8 + i += 1 + } + } + } + + def nextFloat(): Float = { + // > Uses the 24 high-order bits from a call to nextInt() + val bits = nextInt() >>> (32 - 24) + bits.toFloat * (1.0f / (1 << 24)) // lossless multiplication + } + + def nextFloat(bound: Float): Float = { + // false for NaN + if (bound > 0 && bound != Float.PositiveInfinity) + ensureBelowBound(nextFloatBoundedInternal(bound), bound) + else + throw new IllegalArgumentException(s"Illegal bound: $bound") + } + + def nextFloat(origin: Float, bound: Float): Float = { + // `origin < bound` is false if either input is NaN + if (origin != Float.NegativeInfinity && origin < bound && bound != Float.PositiveInfinity) { + val difference = bound - origin + val result = if (difference != Float.PositiveInfinity) { + // Easy case + origin + nextFloatBoundedInternal(difference) + } else { + // Overflow: scale everything down by 0.5 then scale it back up by 2.0 + val halfOrigin = origin * 0.5f + val halfBound = bound * 0.5f + (halfOrigin + nextFloatBoundedInternal(halfBound - halfOrigin)) * 2.0f + } + + ensureBelowBound(result, bound) + } else { + throw new IllegalArgumentException(s"Illegal bounds: [$origin, $bound)") + } + } + + @inline + private def nextFloatBoundedInternal(bound: Float): Float = + nextFloat() * bound + + @inline + private def ensureBelowBound(value: Float, bound: Float): Float = { + /* Based on documentation for Random.doubles to avoid issue #2144 and other + * possible rounding up issues: + * https://docs.oracle.com/javase/8/docs/api/java/util/Random.html#doubles-double-double- + */ + if (value < bound) value + else Math.nextDown(value) + } + + def nextDouble(): Double = { + // > Uses the 53 high-order bits from a call to nextLong() + val bits = nextLong() >>> (64 - 53) + bits.toDouble * (1.0 / (1L << 53)) // lossless multiplication + } + + def nextDouble(bound: Double): Double = { + // false for NaN + if (bound > 0 && bound != Double.PositiveInfinity) + ensureBelowBound(nextDoubleBoundedInternal(bound), bound) + else + throw new IllegalArgumentException(s"Illegal bound: $bound") + } + + def nextDouble(origin: Double, bound: Double): Double = { + // `origin < bound` is false if either input is NaN + if (origin != Double.NegativeInfinity && origin < bound && bound != Double.PositiveInfinity) { + val difference = bound - origin + val result = if (difference != Double.PositiveInfinity) { + // Easy case + origin + nextDoubleBoundedInternal(difference) + } else { + // Overflow: scale everything down by 0.5 then scale it back up by 2.0 + val halfOrigin = origin * 0.5 + val halfBound = bound * 0.5 + (halfOrigin + nextDoubleBoundedInternal(halfBound - halfOrigin)) * 2.0 + } + + ensureBelowBound(result, bound) + } else { + throw new IllegalArgumentException(s"Illegal bounds: [$origin, $bound)") + } + } + + @inline + private def nextDoubleBoundedInternal(bound: Double): Double = + nextDouble() * bound + + @inline + private def ensureBelowBound(value: Double, bound: Double): Double = { + /* Based on documentation for Random.doubles to avoid issue #2144 and other + * possible rounding up issues: + * https://docs.oracle.com/javase/8/docs/api/java/util/Random.html#doubles-double-double- + */ + if (value < bound) value + else Math.nextDown(value) + } + + def nextInt(): Int = { + // > Uses the 32 high-order bits from a call to nextLong() + (nextLong() >>> 32).toInt + } + + /* The algorithms used in nextInt() with bounds were initially part of + * ThreadLocalRandom. That implementation had been written by Doug Lea with + * assistance from members of JCP JSR-166 Expert Group and released to the + * public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + + def nextInt(bound: Int): Int = { + if (bound <= 0) + throw new IllegalArgumentException(s"Illegal bound: $bound") + + nextIntBoundedInternal(bound) + } + + def nextInt(origin: Int, bound: Int): Int = { + if (bound <= origin) + throw new IllegalArgumentException(s"Illegal bounds: [$origin, $bound)") + + val difference = bound - origin + if (difference > 0 || difference == Int.MinValue) { + /* Either the difference did not overflow, or it is the only power of 2 + * that overflows. In both cases, use the straightforward algorithm. + * It works for `MinValue` because the code path for powers of 2 + * basically interprets the bound as unsigned. + */ + origin + nextIntBoundedInternal(difference) + } else { + /* The interval size here is greater than Int.MaxValue, + * so the loop will exit with a probability of at least 1/2. + */ + @tailrec + def loop(): Int = { + val rnd = nextInt() + if (rnd >= origin && rnd < bound) + rnd + else + loop() + } + + loop() + } + } + + private def nextIntBoundedInternal(bound: Int): Int = { + // bound > 0 || bound == Int.MinValue + + if ((bound & -bound) == bound) { // i.e., bound is a power of 2 + // > If bound is a power of two then limiting is a simple masking operation. + nextInt() & (bound - 1) + } else { + /* > Otherwise, the result is re-calculated by invoking nextInt() until + * > the result is greater than or equal zero and less than bound. + */ + + /* Taken literally, that spec would lead to huge rejection rates for + * small bounds. + * Instead, we start from a random 31-bit (non-negative) int `rnd`, and + * we compute `rnd % bound`. + * In order to get a uniform distribution, we must reject and retry if + * we get an `rnd` that is >= the largest int multiple of `bound`. + */ + + @tailrec + def loop(): Int = { + val rnd = nextInt() >>> 1 + val value = rnd % bound // candidate result + + // largest multiple of bound that is <= rnd + val multiple = rnd - value + + // if multiple + bound overflows + if (multiple + bound < 0) { + /* then `multiple` is the largest multiple of bound, and + * `rnd >= multiple`, so we must retry. + */ + loop() + } else { + value + } + } + + loop() + } + } + + // The only abstract method of RandomGenerator + def nextLong(): Long + + /* The algorithms for nextLong() with bounds are copy-pasted from the ones + * for nextInt(), mutatis mutandis. + */ + + def nextLong(bound: Long): Long = { + if (bound <= 0) + throw new IllegalArgumentException(s"Illegal bound: $bound") + + nextLongBoundedInternal(bound) + } + + def nextLong(origin: Long, bound: Long): Long = { + if (bound <= origin) + throw new IllegalArgumentException(s"Illegal bounds: [$origin, $bound)") + + val difference = bound - origin + if (difference > 0 || difference == Long.MinValue) { + /* Either the difference did not overflow, or it is the only power of 2 + * that overflows. In both cases, use the straightforward algorithm. + * It works for `MinValue` because the code path for powers of 2 + * basically interprets the bound as unsigned. + */ + origin + nextLongBoundedInternal(difference) + } else { + /* The interval size here is greater than Long.MaxValue, + * so the loop will exit with a probability of at least 1/2. + */ + @tailrec + def loop(): Long = { + val rnd = nextLong() + if (rnd >= origin && rnd < bound) + rnd + else + loop() + } + + loop() + } + } + + private def nextLongBoundedInternal(bound: Long): Long = { + // bound > 0 || bound == Long.MinValue + + if ((bound & -bound) == bound) { // i.e., bound is a power of 2 + // > If bound is a power of two then limiting is a simple masking operation. + nextLong() & (bound - 1L) + } else { + /* > Otherwise, the result is re-calculated by invoking nextLong() until + * > the result is greater than or equal zero and less than bound. + */ + + /* Taken literally, that spec would lead to huge rejection rates for + * small bounds. + * Instead, we start from a random 63-bit (non-negative) int `rnd`, and + * we compute `rnd % bound`. + * In order to get a uniform distribution, we must reject and retry if + * we get an `rnd` that is >= the largest int multiple of `bound`. + */ + + @tailrec + def loop(): Long = { + val rnd = nextLong() >>> 1 + val value = rnd % bound // candidate result + + // largest multiple of bound that is <= rnd + val multiple = rnd - value + + // if multiple + bound overflows + if (multiple + bound < 0L) { + /* then `multiple` is the largest multiple of bound, and + * `rnd >= multiple`, so we must retry. + */ + loop() + } else { + value + } + } + + loop() + } + } + + // Not implemented + // def nextGaussian(): Double = ??? + // def nextGaussian(mean: Double, stddev: Double): Double = ??? + // def nextExponential(): Double = ??? +} + +object RandomGenerator { // scalastyle:ignore + // Not implemented + // def of(name: String): RandomGenerator = ??? + // def getDefault(): RandomGenerator = ??? +} diff --git a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala index 85216af945..3d2b480a94 100644 --- a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala +++ b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala @@ -182,7 +182,7 @@ private[regex] object IndicesBuilder { final def propagateFromEnd(matchResult: js.RegExp.ExecResult, indices: IndicesArray, end: Int): Unit = { - val start = undefOrFold(matchResult(newGroup))(-1)(matched => end - matched.length) + val start = undefOrFold(matchResult(newGroup))(() => -1)(matched => end - matched.length) propagate(matchResult, indices, start, end) } @@ -194,7 +194,7 @@ private[regex] object IndicesBuilder { final def propagateFromStart(matchResult: js.RegExp.ExecResult, indices: IndicesArray, start: Int): Int = { - val end = undefOrFold(matchResult(newGroup))(-1)(matched => start + matched.length) + val end = undefOrFold(matchResult(newGroup))(() => -1)(matched => start + matched.length) propagate(matchResult, indices, start, end) end } diff --git a/javalib/src/main/scala/java/util/regex/Matcher.scala b/javalib/src/main/scala/java/util/regex/Matcher.scala index 07f94a8076..5acda2c9bb 100644 --- a/javalib/src/main/scala/java/util/regex/Matcher.scala +++ b/javalib/src/main/scala/java/util/regex/Matcher.scala @@ -190,7 +190,7 @@ final class Matcher private[regex] ( pattern().getIndices(ensureLastMatch, lastMatchIsForMatches) private def startInternal(compiledGroup: Int): Int = - undefOrFold(indices(compiledGroup))(-1)(_._1 + regionStart()) + undefOrFold(indices(compiledGroup))(() => -1)(_._1 + regionStart()) def start(group: Int): Int = startInternal(pattern().numberedGroup(group)) @@ -199,7 +199,7 @@ final class Matcher private[regex] ( startInternal(pattern().namedGroup(name)) private def endInternal(compiledGroup: Int): Int = - undefOrFold(indices(compiledGroup))(-1)(_._2 + regionStart()) + undefOrFold(indices(compiledGroup))(() => -1)(_._2 + regionStart()) def end(group: Int): Int = endInternal(pattern().numberedGroup(group)) @@ -278,10 +278,10 @@ object Matcher { */ def start(group: Int): Int = - undefOrFold(indices(pattern.numberedGroup(group)))(-1)(_._1 + regionStart) + undefOrFold(indices(pattern.numberedGroup(group)))(() => -1)(_._1 + regionStart) def end(group: Int): Int = - undefOrFold(indices(pattern.numberedGroup(group)))(-1)(_._2 + regionStart) + undefOrFold(indices(pattern.numberedGroup(group)))(() => -1)(_._2 + regionStart) def group(group: Int): String = undefOrGetOrNull(ensureLastMatch(pattern.numberedGroup(group))) diff --git a/javalib/src/main/scala/java/util/regex/Pattern.scala b/javalib/src/main/scala/java/util/regex/Pattern.scala index 05ee30db58..52dcc3e8f0 100644 --- a/javalib/src/main/scala/java/util/regex/Pattern.scala +++ b/javalib/src/main/scala/java/util/regex/Pattern.scala @@ -133,7 +133,7 @@ final class Pattern private[regex] ( } private[regex] def namedGroup(name: String): Int = { - groupNumberMap(dictGetOrElse(namedGroups, name) { + groupNumberMap(dictGetOrElse(namedGroups, name) { () => throw new IllegalArgumentException(s"No group with name <$name>") }) } diff --git a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala index b2f001407f..751f2e8f78 100644 --- a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala +++ b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala @@ -30,7 +30,7 @@ import java.util.ScalaOps._ import scala.scalajs.js import scala.scalajs.js.JSStringOps.enableJSStringOps -import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo import scala.scalajs.LinkingInfo.ESVersion /** Compiler from Java regular expressions to JavaScript regular expressions. @@ -83,15 +83,15 @@ private[regex] object PatternCompiler { /** Cache for `Support.supportsUnicode`. */ private val _supportsUnicode = - (linkingInfo.esVersion >= ESVersion.ES2015) || featureTest("u") + (LinkingInfo.esVersion >= ESVersion.ES2015) || featureTest("u") /** Cache for `Support.supportsSticky`. */ private val _supportsSticky = - (linkingInfo.esVersion >= ESVersion.ES2015) || featureTest("y") + (LinkingInfo.esVersion >= ESVersion.ES2015) || featureTest("y") /** Cache for `Support.supportsDotAll`. */ private val _supportsDotAll = - (linkingInfo.esVersion >= ESVersion.ES2018) || featureTest("us") + (LinkingInfo.esVersion >= ESVersion.ES2018) || featureTest("us") /** Cache for `Support.supportsIndices`. */ private val _supportsIndices = @@ -107,17 +107,17 @@ private[regex] object PatternCompiler { /** Tests whether the underlying JS RegExp supports the 'u' flag. */ @inline def supportsUnicode: Boolean = - (linkingInfo.esVersion >= ESVersion.ES2015) || _supportsUnicode + (LinkingInfo.esVersion >= ESVersion.ES2015) || _supportsUnicode /** Tests whether the underlying JS RegExp supports the 'y' flag. */ @inline def supportsSticky: Boolean = - (linkingInfo.esVersion >= ESVersion.ES2015) || _supportsSticky + (LinkingInfo.esVersion >= ESVersion.ES2015) || _supportsSticky /** Tests whether the underlying JS RegExp supports the 's' flag. */ @inline def supportsDotAll: Boolean = - (linkingInfo.esVersion >= ESVersion.ES2018) || _supportsDotAll + (LinkingInfo.esVersion >= ESVersion.ES2018) || _supportsDotAll /** Tests whether the underlying JS RegExp supports the 'd' flag. */ @inline @@ -131,7 +131,7 @@ private[regex] object PatternCompiler { */ @inline def enableUnicodeCaseInsensitive: Boolean = - linkingInfo.esVersion >= ESVersion.ES2015 + LinkingInfo.esVersion >= ESVersion.ES2015 /** Tests whether features requiring \p{} and/or look-behind assertions are enabled. * @@ -140,7 +140,7 @@ private[regex] object PatternCompiler { */ @inline def enableUnicodeCharacterClassesAndLookBehinds: Boolean = - linkingInfo.esVersion >= ESVersion.ES2018 + LinkingInfo.esVersion >= ESVersion.ES2018 } import Support._ @@ -215,7 +215,7 @@ private[regex] object PatternCompiler { import InlinedHelpers._ private def codePointToString(codePoint: Int): String = { - if (linkingInfo.esVersion >= ESVersion.ES2015) { + if (LinkingInfo.esVersion >= ESVersion.ES2015) { js.Dynamic.global.String.fromCodePoint(codePoint).asInstanceOf[String] } else { if (isBmpCodePoint(codePoint)) { @@ -1385,7 +1385,7 @@ private final class PatternCompiler(private val pattern: String, private var fla parseError("\\k is not followed by '<' for named capturing group") pIndex += 1 val groupName = parseGroupName() - val groupNumber = dictGetOrElse(namedGroups, groupName) { + val groupNumber = dictGetOrElse(namedGroups, groupName) { () => parseError(s"named capturing group <$groupName> does not exit") } val compiledGroupNumber = groupNumberMap(groupNumber) @@ -1639,7 +1639,7 @@ private final class PatternCompiler(private val pattern: String, private var fla // For anything else, we need built-in support for \p requireES2018Features("Unicode character family") - mapGetOrElse(predefinedPCharacterClasses, property) { + mapGetOrElse(predefinedPCharacterClasses, property) { () => val scriptPrefixLen = if (property.startsWith("Is")) { 2 } else if (property.startsWith("sc=")) { @@ -1674,7 +1674,7 @@ private final class PatternCompiler(private val pattern: String, private var fla val lowercase = scriptName.toLowerCase() - mapGetOrElseUpdate(canonicalizedScriptNameCache, lowercase) { + mapGetOrElseUpdate(canonicalizedScriptNameCache, lowercase) { () => val canonical = lowercase.jsReplace(scriptCanonicalizeRegExp, ((s: String) => s.toUpperCase()): js.Function1[String, String]) diff --git a/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala b/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala index 945753d91b..e0bd4e1223 100644 --- a/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala +++ b/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala @@ -13,7 +13,6 @@ package java.util.regex import scala.scalajs.js -import scala.scalajs.runtime.linkingInfo import scala.scalajs.LinkingInfo class PatternSyntaxException(desc: String, regex: String, index: Int) diff --git a/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala b/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala index 4673a4cf9e..015c328818 100644 --- a/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala +++ b/junit-runtime/src/main/scala/org/scalajs/junit/Reporter.scala @@ -37,7 +37,7 @@ private[junit] final class Reporter(eventHandler: EventHandler, def reportIgnored(method: Option[String]): Unit = { logTestInfo(_.info, method, "ignored") - emitEvent(method, Status.Skipped) + emitEvent(method, Status.Skipped, 0, None) } def reportTestStarted(method: String): Unit = @@ -47,7 +47,7 @@ private[junit] final class Reporter(eventHandler: EventHandler, logTestInfo(_.debug, Some(method), s"finished, took $timeInSeconds sec") if (succeeded) - emitEvent(Some(method), Status.Success) + emitEvent(Some(method), Status.Success, timeInSeconds, None) } def reportErrors(prefix: String, method: Option[String], @@ -59,7 +59,7 @@ private[junit] final class Reporter(eventHandler: EventHandler, if (errors.nonEmpty) { emit(errors.head) - emitEvent(method, Status.Failure) + emitEvent(method, Status.Failure, timeInSeconds, Some(errors.head)) errors.tail.foreach(emit) } } @@ -67,7 +67,7 @@ private[junit] final class Reporter(eventHandler: EventHandler, def reportAssumptionViolation(method: Option[String], timeInSeconds: Double, e: Throwable): Unit = { logTestException(_.warn, "Test assumption in test ", method, e, timeInSeconds) - emitEvent(method, Status.Skipped) + emitEvent(method, Status.Skipped, timeInSeconds, Some(e)) } private def logTestInfo(level: Reporter.Level, method: Option[String], msg: String): Unit = @@ -114,11 +114,18 @@ private[junit] final class Reporter(eventHandler: EventHandler, prefix + Ansi.c(name, color) } - private def emitEvent(method: Option[String], status: Status): Unit = { + private def emitEvent( + method: Option[String], + status: Status, + timeInSeconds: Double, + throwable: Option[Throwable] + ): Unit = { val testName = method.fold(taskDef.fullyQualifiedName())(method => taskDef.fullyQualifiedName() + "." + settings.decodeName(method)) val selector = new TestSelector(testName) - eventHandler.handle(new JUnitEvent(taskDef, status, selector)) + val optionalThrowable: OptionalThrowable = new OptionalThrowable(throwable.orNull) + val duration: Long = (timeInSeconds*1000).toLong + eventHandler.handle(new JUnitEvent(taskDef, status, selector, optionalThrowable, duration)) } def log(level: Reporter.Level, s: String): Unit = { diff --git a/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt b/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt index 2a161db9ae..1890d11297 100644 --- a/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt +++ b/junit-test/outputs/org/scalajs/junit/AssertEquals2TestAssertions_.txt @@ -1,7 +1,7 @@ ldTest run started ldTest org.scalajs.junit.AssertEquals2Test.test started leTest org.scalajs.junit.AssertEquals2Test.test failed: This is the message expected: but was:, took