From 572a1bd962c2a6fa077149ae25d96426e1f07253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 25 Jun 2022 14:12:21 +0200 Subject: [PATCH 01/75] Towards 1.10.2. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- project/Build.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 6a9cf8bd48..c45c54b3e2 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.10.1", + current = "1.10.2-SNAPSHOT", binaryEmitted = "1.8" ) diff --git a/project/Build.scala b/project/Build.scala index 0f21d82e7d..7ae2b163c8 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -244,7 +244,7 @@ object Build { val previousVersions = List("1.0.0", "1.0.1", "1.1.0", "1.1.1", "1.2.0", "1.3.0", "1.3.1", "1.4.0", "1.5.0", "1.5.1", "1.6.0", "1.7.0", "1.7.1", - "1.8.0", "1.9.0", "1.10.0") + "1.8.0", "1.9.0", "1.10.0", "1.10.1") val previousVersion = previousVersions.last val previousBinaryCrossVersion = CrossVersion.binaryWith("sjs1_", "") From 0ca8964b3656e9e20aa276de5dc84173135a8590 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jun 2022 06:14:13 +0000 Subject: [PATCH 02/75] Bump jsdom from 16.3.0 to 16.5.0 Bumps [jsdom](https://github.com/jsdom/jsdom) from 16.3.0 to 16.5.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/16.3.0...16.5.0) --- updated-dependencies: - dependency-name: jsdom dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 292 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 144 insertions(+), 150 deletions(-) diff --git a/package-lock.json b/package-lock.json index 159642ca26..0331fea1e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,15 +3,15 @@ "lockfileVersion": 1, "dependencies": { "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, "acorn": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", - "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true }, "acorn-globals": { @@ -22,6 +22,14 @@ "requires": { "acorn": "^7.1.1", "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + } } }, "acorn-walk": { @@ -31,9 +39,9 @@ "dev": true }, "ajv": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", - "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -43,9 +51,9 @@ } }, "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, "requires": { "safer-buffer": "~2.1.0" @@ -54,31 +62,31 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true }, "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, "requires": { "tweetnacl": "^0.14.3" @@ -99,7 +107,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, "colors": { @@ -149,7 +157,7 @@ "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, "requires": { "assert-plus": "^1.0.0" @@ -167,21 +175,21 @@ } }, "decimal.js": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz", - "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", + "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", "dev": true }, "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, "domexception": { @@ -204,7 +212,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, "requires": { "jsbn": "~0.1.0", @@ -212,13 +220,13 @@ } }, "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", "dev": true, "requires": { "esprima": "^4.0.1", - "estraverse": "^4.2.0", + "estraverse": "^5.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" @@ -231,9 +239,9 @@ "dev": true }, "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, "esutils": { @@ -251,7 +259,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true }, "fast-deep-equal": { @@ -269,13 +277,13 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true }, "form-data": { @@ -292,7 +300,7 @@ "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, "requires": { "assert-plus": "^1.0.0" @@ -301,16 +309,16 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, @@ -326,7 +334,7 @@ "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "dev": true, "requires": { "assert-plus": "^1.0.0", @@ -355,22 +363,16 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, "is-potential-custom-element-name": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", - "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, "isarray": { @@ -382,53 +384,53 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, "jsdom": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.3.0.tgz", - "integrity": "sha512-zggeX5UuEknpdZzv15+MS1dPYG0J/TftiiNunOeNxSl3qr8Z6cIlQpN0IdJa44z9aFxZRIVqRncvEhQ7X5DtZg==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.5.0.tgz", + "integrity": "sha512-QxZH0nmDTnTTVI0YDm4RUlaUPl5dcyn62G5TMDNfMmTW+J1u1v9gCR8WR+WZ6UghAa7nKJjDOFaI00eMMWvJFQ==", "dev": true, "requires": { - "abab": "^2.0.3", - "acorn": "^7.1.1", + "abab": "^2.0.5", + "acorn": "^8.0.5", "acorn-globals": "^6.0.0", "cssom": "^0.4.4", - "cssstyle": "^2.2.0", + "cssstyle": "^2.3.0", "data-urls": "^2.0.0", - "decimal.js": "^10.2.0", + "decimal.js": "^10.2.1", "domexception": "^2.0.1", - "escodegen": "^1.14.1", + "escodegen": "^2.0.0", "html-encoding-sniffer": "^2.0.1", "is-potential-custom-element-name": "^1.0.0", "nwsapi": "^2.2.0", - "parse5": "5.1.1", + "parse5": "6.0.1", "request": "^2.88.2", - "request-promise-native": "^1.0.8", - "saxes": "^5.0.0", + "request-promise-native": "^1.0.9", + "saxes": "^5.0.1", "symbol-tree": "^3.2.4", - "tough-cookie": "^3.0.1", + "tough-cookie": "^4.0.0", "w3c-hr-time": "^1.0.2", "w3c-xmlserializer": "^2.0.0", "webidl-conversions": "^6.1.0", "whatwg-encoding": "^1.0.5", "whatwg-mimetype": "^2.3.0", "whatwg-url": "^8.0.0", - "ws": "^7.2.3", + "ws": "^7.4.4", "xml-name-validator": "^3.0.0" } }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, "json-schema-traverse": { @@ -440,18 +442,18 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -470,7 +472,7 @@ "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, "requires": { "prelude-ls": "~1.1.2", @@ -492,12 +494,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -505,18 +501,18 @@ "dev": true }, "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "requires": { - "mime-db": "1.44.0" + "mime-db": "1.52.0" } }, "minimist": { @@ -537,9 +533,9 @@ } }, "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz", + "integrity": "sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==", "dev": true }, "oauth-sign": { @@ -579,21 +575,21 @@ "dev": true }, "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true }, "process-nextick-args": { @@ -615,9 +611,9 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true }, "readable-stream": { @@ -684,21 +680,21 @@ } }, "request-promise-core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", - "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "dev": true, "requires": { - "lodash": "^4.17.15" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", - "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "dev": true, "requires": { - "request-promise-core": "1.1.3", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, @@ -759,9 +755,9 @@ } }, "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -778,7 +774,7 @@ "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", "dev": true }, "string_decoder": { @@ -805,20 +801,20 @@ "dev": true }, "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", "dev": true, "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" } }, "tr46": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", - "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", "dev": true, "requires": { "punycode": "^2.1.1" @@ -827,7 +823,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "requires": { "safe-buffer": "^5.0.1" @@ -836,22 +832,28 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", "dev": true, "requires": { "prelude-ls": "~1.1.2" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -872,7 +874,7 @@ "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "dev": true, "requires": { "assert-plus": "^1.0.0", @@ -920,22 +922,14 @@ "dev": true }, "whatwg-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz", - "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", "dev": true, "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^2.0.2", - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" } }, "word-wrap": { @@ -951,9 +945,9 @@ "dev": true }, "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", "dev": true }, "xml-name-validator": { diff --git a/package.json b/package.json index 59aaf004f7..77e056fa88 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "devDependencies": { "source-map-support": "0.5.19", "jszip": "3.7.0", - "jsdom": "16.3.0", + "jsdom": "16.5.0", "node-static": "0.7.11" } } From 767c415a4279f76fb6c29dc0708c9642cbc780d7 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 26 Jun 2022 10:41:05 +0200 Subject: [PATCH 03/75] Replace obsolete cancel with assert The cancel branch is not necessary anymore since 619f264e431b079e8cfc3393a8744514c8a1577a. Discovered while working on #2227. --- .../linker/frontend/optimizer/OptimizerCore.scala | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index c6742f5999..2c9605c148 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -1174,19 +1174,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case PreTransLocalDef(LocalDef(_, _, InlineClassInstanceReplacement(_, fieldLocalDefs, cancelFun))) => val fieldLocalDef = fieldLocalDefs(FieldID(className, field)) - if (!isLhsOfAssign || fieldLocalDef.mutable) { - cont(fieldLocalDef.toPreTransform) - } else { - /* In an ideal world, this should not happen (assigning to an - * immutable field of an already constructed object). However, since - * we cannot IR-check that this does not happen (see #1021), this is - * effectively allowed by the IR spec. We are therefore not allowed - * to crash. We cancel instead. This will become an actual field - * (rather than an optimized local val) which is not considered pure - * (for that same reason). - */ - cancelFun() - } + assert(!isLhsOfAssign || fieldLocalDef.mutable, s"assign to immutable field at $pos") + cont(fieldLocalDef.toPreTransform) // Select the lo or hi "field" of a Long literal case PreTransLit(LongLiteral(value)) if useRuntimeLong => From ca017ea2c487140e514bbdf9d20d54b42dc5bed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 9 Jun 2022 17:05:29 +0200 Subject: [PATCH 04/75] IR cleaner: rewrite some intrinsic implicit conversions as no-ops. There are a number of implicit conversions in the Scala.js standard library that basically amount to no-ops at run-time. Previously, we could not directly use them from the javalanglib, because they would not pass the IR cleaner. This caused a mix of re-implementation and forced casts at call sites. We improve the IR cleaner to identify calls to those "intrinsics", and directly rewrite them as no-ops. This allows to simplify and/or make safer a number of sites in the javalanglib. In addition to no-op conversions, we also handle the `truthValue` conversion, so that we complete the set of operations provided by `js.DynamicImplicits`. --- .../src/main/scala/java/lang/Character.scala | 14 +-- .../src/main/scala/java/lang/Double.scala | 19 ++-- .../src/main/scala/java/lang/Float.scala | 2 +- .../scala/java/lang/FloatingPointBits.scala | 6 +- .../src/main/scala/java/lang/Integer.scala | 19 ++-- .../src/main/scala/java/lang/Long.scala | 7 +- .../src/main/scala/java/lang/StackTrace.scala | 4 +- .../src/main/scala/java/lang/System.scala | 8 +- .../src/main/scala/java/lang/Utils.scala | 26 +---- .../src/main/scala/java/lang/_String.scala | 22 ++--- .../scalajs/linker/runtime/RuntimeLong.scala | 14 ++- project/JavalibIRCleaner.scala | 95 ++++++++++++++++--- 12 files changed, 138 insertions(+), 98 deletions(-) diff --git a/javalanglib/src/main/scala/java/lang/Character.scala b/javalanglib/src/main/scala/java/lang/Character.scala index de01826d1b..b260948a6d 100644 --- a/javalanglib/src/main/scala/java/lang/Character.scala +++ b/javalanglib/src/main/scala/java/lang/Character.scala @@ -117,23 +117,17 @@ object Character { @inline def hashCode(value: Char): Int = value.toInt - @inline def toString(c: Char): String = { - js.Dynamic.global.String - .fromCharCode(c.toInt.asInstanceOf[js.Dynamic]) - .asInstanceOf[String] - } + @inline def toString(c: Char): String = + js.Dynamic.global.String.fromCharCode(c.toInt).asInstanceOf[String] def toString(codePoint: Int): String = { if (isBmpCodePoint(codePoint)) { js.Dynamic.global.String - .fromCharCode(codePoint.asInstanceOf[js.Dynamic]) + .fromCharCode(codePoint) .asInstanceOf[String] } else if (isValidCodePoint(codePoint)) { js.Dynamic.global.String - .fromCharCode( - highSurrogate(codePoint).toInt.asInstanceOf[js.Dynamic], - lowSurrogate(codePoint).toInt.asInstanceOf[js.Dynamic] - ) + .fromCharCode(highSurrogate(codePoint).toInt, lowSurrogate(codePoint).toInt) .asInstanceOf[String] } else { throw new IllegalArgumentException() diff --git a/javalanglib/src/main/scala/java/lang/Double.scala b/javalanglib/src/main/scala/java/lang/Double.scala index ae6140aff8..094cfb8315 100644 --- a/javalanglib/src/main/scala/java/lang/Double.scala +++ b/javalanglib/src/main/scala/java/lang/Double.scala @@ -16,6 +16,8 @@ import java.lang.constant.{Constable, ConstantDesc} import scala.scalajs.js +import Utils._ + /* This is a hijacked class. Its instances are primitive numbers. * Constructors are not emitted. */ @@ -106,7 +108,7 @@ object Double { def parseDouble(s: String): scala.Double = { val groups = doubleStrPat.exec(s) if (groups != null) - js.Dynamic.global.parseFloat(groups(1).asInstanceOf[js.Any]).asInstanceOf[scala.Double] + js.Dynamic.global.parseFloat(undefOrForceGet[String](groups(1))).asInstanceOf[scala.Double] else parseDoubleSlowPath(s) } @@ -120,10 +122,10 @@ object Double { if (groups == null) fail() - val signStr = groups(1).asInstanceOf[String] - val integralPartStr = groups(2).asInstanceOf[String] - val fractionalPartStr = groups(3).asInstanceOf[String] - val binaryExpStr = groups(4).asInstanceOf[String] + val signStr = undefOrForceGet(groups(1)) + val integralPartStr = undefOrForceGet(groups(2)) + val fractionalPartStr = undefOrForceGet(groups(3)) + val binaryExpStr = undefOrForceGet(groups(4)) if (integralPartStr == "" && fractionalPartStr == "") fail() @@ -236,11 +238,8 @@ object Double { * `binaryExp` (see below) did not stray too far from that range itself. */ - @inline def nativeParseInt(s: String, radix: Int): scala.Double = { - js.Dynamic.global - .parseInt(s.asInstanceOf[js.Any], radix.asInstanceOf[js.Any]) - .asInstanceOf[scala.Double] - } + @inline def nativeParseInt(s: String, radix: Int): scala.Double = + js.Dynamic.global.parseInt(s, radix).asInstanceOf[scala.Double] val mantissa = nativeParseInt(truncatedMantissaStr, 16) // Assert: mantissa != 0.0 && mantissa != scala.Double.PositiveInfinity diff --git a/javalanglib/src/main/scala/java/lang/Float.scala b/javalanglib/src/main/scala/java/lang/Float.scala index 22dc286415..523bd8ed1a 100644 --- a/javalanglib/src/main/scala/java/lang/Float.scala +++ b/javalanglib/src/main/scala/java/lang/Float.scala @@ -145,7 +145,7 @@ object Float { integralPartStr: String, fractionalPartStr: String, exponentStr: String): scala.Float = { - val z0 = js.Dynamic.global.parseFloat(fullNumberStr.asInstanceOf[js.Any]).asInstanceOf[scala.Double] + val z0 = js.Dynamic.global.parseFloat(fullNumberStr).asInstanceOf[scala.Double] val z = z0.toFloat val zDouble = z.toDouble diff --git a/javalanglib/src/main/scala/java/lang/FloatingPointBits.scala b/javalanglib/src/main/scala/java/lang/FloatingPointBits.scala index 359991af0e..3b3f972346 100644 --- a/javalanglib/src/main/scala/java/lang/FloatingPointBits.scala +++ b/javalanglib/src/main/scala/java/lang/FloatingPointBits.scala @@ -348,7 +348,9 @@ private[lang] object FloatingPointBits { } } - @inline private def rawToInt(x: scala.Double): Int = - (x.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int] + @inline private def rawToInt(x: scala.Double): Int = { + import scala.scalajs.js.DynamicImplicits.number2dynamic + (x | 0).asInstanceOf[Int] + } } diff --git a/javalanglib/src/main/scala/java/lang/Integer.scala b/javalanglib/src/main/scala/java/lang/Integer.scala index 531aef701b..dbd6ba89ee 100644 --- a/javalanglib/src/main/scala/java/lang/Integer.scala +++ b/javalanglib/src/main/scala/java/lang/Integer.scala @@ -299,7 +299,7 @@ object Integer { if (radix == 10 || radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) { Integer.toString(i) } else { - import Utils.Implicits.enableJSNumberOps + import js.JSNumberOps.enableJSNumberOps i.toString(radix) } } @@ -313,14 +313,17 @@ object Integer { @inline def min(a: Int, b: Int): Int = Math.min(a, b) @inline private[this] def toStringBase(i: scala.Int, base: scala.Int): String = { - asUint(i).asInstanceOf[js.Dynamic] - .applyDynamic("toString")(base.asInstanceOf[js.Dynamic]) - .asInstanceOf[String] + import js.JSNumberOps.enableJSNumberOps + asUint(i).toString(base) } - @inline private def asInt(n: scala.Double): scala.Int = - (n.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int] + @inline private def asInt(n: scala.Double): scala.Int = { + import js.DynamicImplicits.number2dynamic + (n | 0).asInstanceOf[Int] + } - @inline private def asUint(n: scala.Int): scala.Double = - (n.asInstanceOf[js.Dynamic] >>> 0.asInstanceOf[js.Dynamic]).asInstanceOf[scala.Double] + @inline private def asUint(n: scala.Int): scala.Double = { + import js.DynamicImplicits.number2dynamic + (n.toDouble >>> 0).asInstanceOf[scala.Double] + } } diff --git a/javalanglib/src/main/scala/java/lang/Long.scala b/javalanglib/src/main/scala/java/lang/Long.scala index 9330eb722e..cd08a8c839 100644 --- a/javalanglib/src/main/scala/java/lang/Long.scala +++ b/javalanglib/src/main/scala/java/lang/Long.scala @@ -144,7 +144,7 @@ object Long { val hi = (i >>> 32).toInt if (lo >> 31 == hi) { // It's a signed int32 - import Utils.Implicits.enableJSNumberOps + import js.JSNumberOps.enableJSNumberOps lo.toString(radix) } else if (hi < 0) { "-" + toUnsignedStringInternalLarge(-i, radix) @@ -157,7 +157,7 @@ object Long { private def toUnsignedStringImpl(i: scala.Long, radix: Int): String = { if ((i >>> 32).toInt == 0) { // It's an unsigned int32 - import Utils.Implicits.enableJSNumberOps + import js.JSNumberOps.enableJSNumberOps Utils.toUint(i.toInt).toString(radix) } else { toUnsignedStringInternalLarge(i, radix) @@ -166,7 +166,8 @@ object Long { // Must be called only with valid radix private def toUnsignedStringInternalLarge(i: scala.Long, radix: Int): String = { - import Utils.Implicits._ + import js.JSNumberOps.enableJSNumberOps + import js.JSStringOps.enableJSStringOps val radixInfo = StringRadixInfos(radix) val divisor = radixInfo.radixPowLength diff --git a/javalanglib/src/main/scala/java/lang/StackTrace.scala b/javalanglib/src/main/scala/java/lang/StackTrace.scala index ef7fac256e..dbbd654842 100644 --- a/javalanglib/src/main/scala/java/lang/StackTrace.scala +++ b/javalanglib/src/main/scala/java/lang/StackTrace.scala @@ -15,9 +15,9 @@ package java.lang import scala.annotation.tailrec import scala.scalajs.js +import scala.scalajs.js.JSStringOps.enableJSStringOps import Utils._ -import Utils.Implicits.enableJSStringOps /** Conversions of JavaScript stack traces to Java stack traces. */ @@ -304,7 +304,7 @@ private[lang] object StackTrace { */ private def normalizeStackTraceLines(e: js.Dynamic): js.Array[String] = { - import Utils.DynamicImplicits.{truthValue, number2dynamic} + import js.DynamicImplicits.{truthValue, number2dynamic} /* You would think that we could test once and for all which "mode" to * adopt. But the format can actually differ for different exceptions diff --git a/javalanglib/src/main/scala/java/lang/System.scala b/javalanglib/src/main/scala/java/lang/System.scala index 0a4bb25fd5..954f5ec050 100644 --- a/javalanglib/src/main/scala/java/lang/System.scala +++ b/javalanglib/src/main/scala/java/lang/System.scala @@ -71,7 +71,7 @@ object System { private object NanoTime { val getHighPrecisionTime: js.Function0[scala.Double] = { - import Utils.DynamicImplicits.truthValue + import js.DynamicImplicits.truthValue if (js.typeOf(global.performance) != "undefined") { if (global.performance.now) { @@ -382,13 +382,13 @@ private final class JSConsoleBasedPrintStream(isErr: scala.Boolean) override def close(): Unit = () private def doWriteLine(line: String): Unit = { - import Utils.DynamicImplicits.truthValue + import js.DynamicImplicits.truthValue if (js.typeOf(global.console) != "undefined") { if (isErr && global.console.error) - global.console.error(line.asInstanceOf[js.Any]) + global.console.error(line) else - global.console.log(line.asInstanceOf[js.Any]) + global.console.log(line) } } } diff --git a/javalanglib/src/main/scala/java/lang/Utils.scala b/javalanglib/src/main/scala/java/lang/Utils.scala index 04438b6ec6..5831f160d2 100644 --- a/javalanglib/src/main/scala/java/lang/Utils.scala +++ b/javalanglib/src/main/scala/java/lang/Utils.scala @@ -132,28 +132,8 @@ private[lang] object Utils { } } - @inline def toUint(x: scala.Double): scala.Double = - (x.asInstanceOf[js.Dynamic] >>> 0.asInstanceOf[js.Dynamic]).asInstanceOf[scala.Double] - - object Implicits { - implicit def enableJSStringOps(x: String): js.JSStringOps = - x.asInstanceOf[js.JSStringOps] - - implicit def enableJSNumberOps(x: Int): js.JSNumberOps = - x.asInstanceOf[js.JSNumberOps] - - implicit def enableJSNumberOps(x: scala.Double): js.JSNumberOps = - x.asInstanceOf[js.JSNumberOps] - } - - object DynamicImplicits { - @inline implicit def truthValue(x: js.Dynamic): scala.Boolean = - (!(!x)).asInstanceOf[scala.Boolean] - - implicit def number2dynamic(x: scala.Double): js.Dynamic = - x.asInstanceOf[js.Dynamic] - - implicit def boolean2dynamic(x: scala.Boolean): js.Dynamic = - x.asInstanceOf[js.Dynamic] + @inline def toUint(x: scala.Double): scala.Double = { + import js.DynamicImplicits.number2dynamic + (x >>> 0).asInstanceOf[scala.Double] } } diff --git a/javalanglib/src/main/scala/java/lang/_String.scala b/javalanglib/src/main/scala/java/lang/_String.scala index 2d1ebcee0b..b9e30ac9f4 100644 --- a/javalanglib/src/main/scala/java/lang/_String.scala +++ b/javalanglib/src/main/scala/java/lang/_String.scala @@ -18,6 +18,7 @@ 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.ESVersion @@ -27,8 +28,6 @@ import java.nio.charset.Charset import java.util.Locale import java.util.regex._ -import Utils.Implicits.enableJSStringOps - /* This is the implementation of java.lang.String, which is a hijacked class. * Its instances are primitive strings. Constructors are not emitted. * @@ -48,18 +47,12 @@ final class _String private () // scalastyle:ignore this.asInstanceOf[String] @inline - def charAt(index: Int): Char = { - this.asInstanceOf[js.Dynamic] - .charCodeAt(index.asInstanceOf[js.Dynamic]) - .asInstanceOf[Int] - .toChar - } + def charAt(index: Int): Char = + this.asInstanceOf[js.Dynamic].charCodeAt(index).asInstanceOf[Int].toChar def codePointAt(index: Int): Int = { if (linkingInfo.esVersion >= ESVersion.ES2015) { - this.asInstanceOf[js.Dynamic] - .codePointAt(index.asInstanceOf[js.Dynamic]) - .asInstanceOf[Int] + this.asInstanceOf[js.Dynamic].codePointAt(index).asInstanceOf[Int] } else { val high = charAt(index) if (index+1 < length()) { @@ -377,11 +370,8 @@ final class _String private () // scalastyle:ignore thisString.jsSubstring(beginIndex) @inline - def substring(beginIndex: Int, endIndex: Int): String = { - this.asInstanceOf[js.Dynamic] - .substring(beginIndex.asInstanceOf[js.Dynamic], endIndex.asInstanceOf[js.Dynamic]) - .asInstanceOf[String] - } + def substring(beginIndex: Int, endIndex: Int): String = + thisString.jsSubstring(beginIndex, endIndex) def toCharArray(): Array[Char] = { val len = length() diff --git a/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala b/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala index a0ce273840..7a90e2a9e1 100644 --- a/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala +++ b/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala @@ -1017,10 +1017,8 @@ object RuntimeLong { @inline private def substring(s: String, start: Int): String = { - import scala.scalajs.js - s.asInstanceOf[js.Dynamic] - .substring(start.asInstanceOf[js.Dynamic]) - .asInstanceOf[String] + import scala.scalajs.js.JSStringOps.enableJSStringOps + s.jsSubstring(start) } /** Tests whether the long (lo, hi) is 0. */ @@ -1067,14 +1065,14 @@ object RuntimeLong { * `Double`. */ @inline def asUint(x: Int): Double = { - import scala.scalajs.js - (x.asInstanceOf[js.Dynamic] >>> 0.asInstanceOf[js.Dynamic]).asInstanceOf[Double] + import scala.scalajs.js.DynamicImplicits.number2dynamic + (x.toDouble >>> 0).asInstanceOf[Double] } /** Performs the JavaScript operation `(x | 0)`. */ @inline def rawToInt(x: Double): Int = { - import scala.scalajs.js - (x.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int] + import scala.scalajs.js.DynamicImplicits.number2dynamic + (x | 0).asInstanceOf[Int] } /** Tests whether the given non-zero unsigned Int is an exact power of 2. */ diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 5e6f4fc913..8c73a20194 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -29,16 +29,14 @@ import sbt.{Logger, MessageOnlyException} * `java.lang.Object`. * - Eagerly dereference `LoadJSConstructor` and `LoadJSModule` by "inlining" * the JS load spec of the mentioned class ref. + * - Replace calls to "intrinsic" methods of the Scala.js library by their + * meaning at call site. * * Afterwards, we check that the IR does not contain any reference to classes * under the `scala.*` package. */ final class JavalibIRCleaner(baseDirectoryURI: URI) { - private val JavaIOSerializable = ClassName("java.io.Serializable") - private val ScalaSerializable = ClassName("scala.Serializable") - - private val writeReplaceMethodName = - MethodName("writeReplace", Nil, ClassRef(ObjectClass)) + import JavalibIRCleaner._ def cleanIR(dependencyFiles: Seq[File], libFileMappings: Seq[(File, File)], logger: Logger): Set[File] = { @@ -207,7 +205,55 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { override def transform(tree: Tree, isStat: Boolean): Tree = { implicit val pos = tree.pos - val result = super.transform(tree, isStat) match { + val tree1 = preTransform(tree, isStat) + val tree2 = if (tree1 eq tree) super.transform(tree, isStat) else transform(tree1, isStat) + val result = postTransform(tree2, isStat) + + validateType(result.tpe) + + result + } + + private def preTransform(tree: Tree, isStat: Boolean): Tree = { + implicit val pos = tree.pos + + tree match { + case IntrinsicCall(JSAnyMod, `jsAnyFromIntMethodName`, List(arg)) => + arg + case IntrinsicCall(JSAnyMod, `jsAnyFromStringMethodName`, List(arg)) => + arg + case IntrinsicCall(JSDynamicImplicitsMod, `number2dynamicMethodName`, List(arg)) => + arg + case IntrinsicCall(JSNumberOpsMod, `enableJSNumberOpsDoubleMethodName`, List(arg)) => + arg + case IntrinsicCall(JSNumberOpsMod, `enableJSNumberOpsIntMethodName`, List(arg)) => + arg + case IntrinsicCall(JSStringOpsMod, `enableJSStringOpsMethodName`, List(arg)) => + arg + + case IntrinsicCall(JSDynamicImplicitsMod, `truthValueMethodName`, List(arg)) => + AsInstanceOf( + JSUnaryOp(JSUnaryOp.!, JSUnaryOp(JSUnaryOp.!, arg)), + BooleanType) + + case _ => + tree + } + } + + private object IntrinsicCall { + def unapply(tree: Apply): Option[(ClassName, MethodName, List[Tree])] = tree match { + case Apply(ApplyFlags.empty, LoadModule(moduleClassName), MethodIdent(methodName), args) => + Some(moduleClassName, methodName, args) + case _ => + None + } + } + + private def postTransform(tree: Tree, isStat: Boolean): Tree = { + implicit val pos = tree.pos + + tree match { case New(className, ctor, args) => New(className, transformMethodIdent(ctor), args) @@ -242,12 +288,9 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { reportError(s"illegal ClassOf(${t.typeRef})") t - case t => - t + case _ => + tree } - - validateType(result.tpe) - result } private def genLoadFromLoadSpecOf(className: ClassName)( @@ -374,3 +417,33 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } } } + +object JavalibIRCleaner { + private val JavaIOSerializable = ClassName("java.io.Serializable") + private val JSAny = ClassName("scala.scalajs.js.Any") + private val JSAnyMod = ClassName("scala.scalajs.js.Any$") + private val JSDynamic = ClassName("scala.scalajs.js.Dynamic") + private val JSDynamicImplicitsMod = ClassName("scala.scalajs.js.DynamicImplicits$") + private val JSNumberOps = ClassName("scala.scalajs.js.JSNumberOps") + private val JSNumberOpsMod = ClassName("scala.scalajs.js.JSNumberOps$") + private val JSStringOps = ClassName("scala.scalajs.js.JSStringOps") + private val JSStringOpsMod = ClassName("scala.scalajs.js.JSStringOps$") + private val ScalaSerializable = ClassName("scala.Serializable") + + private val enableJSNumberOpsDoubleMethodName = + MethodName("enableJSNumberOps", List(DoubleRef), ClassRef(JSNumberOps)) + private val enableJSNumberOpsIntMethodName = + MethodName("enableJSNumberOps", List(IntRef), ClassRef(JSNumberOps)) + private val enableJSStringOpsMethodName = + MethodName("enableJSStringOps", List(ClassRef(BoxedStringClass)), ClassRef(JSStringOps)) + private val jsAnyFromIntMethodName = + MethodName("fromInt", List(IntRef), ClassRef(JSAny)) + private val jsAnyFromStringMethodName = + MethodName("fromString", List(ClassRef(BoxedStringClass)), ClassRef(JSAny)) + private val number2dynamicMethodName = + MethodName("number2dynamic", List(DoubleRef), ClassRef(JSDynamic)) + private val truthValueMethodName = + MethodName("truthValue", List(ClassRef(JSDynamic)), BooleanRef) + private val writeReplaceMethodName = + MethodName("writeReplace", Nil, ClassRef(ObjectClass)) +} From 67bc9115e670e9a5b5267fe32cbdc265cf4fa311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 10 Jun 2022 10:23:13 +0200 Subject: [PATCH 05/75] IR cleaner: erase Scala FunctionN's to JavaScript closures. `new AnonFunctionN(closure)` is replaced by `closure`, while `someFunctionN(args)` is replaced by `someFunctionN(args)`. All function types are rewritten to `any`, and all other references to the function classes are rewritten to `java.lang.Object`. This requires that we compile with `-no-specialization` in order not to get calls to the specialized `apply` methods. These changes allow to use regular Scala functions, with the arrow notation, in the javalanglib, including by-name parameters. --- .../src/main/scala/java/lang/ClassValue.scala | 2 +- .../src/main/scala/java/lang/Float.scala | 10 +- .../src/main/scala/java/lang/Integer.scala | 2 +- .../src/main/scala/java/lang/StackTrace.scala | 4 +- .../src/main/scala/java/lang/System.scala | 4 +- .../src/main/scala/java/lang/Throwables.scala | 2 +- .../src/main/scala/java/lang/Utils.scala | 11 +- .../src/main/scala/java/lang/_String.scala | 6 +- project/Build.scala | 3 + project/JavalibIRCleaner.scala | 195 +++++++++++++----- 10 files changed, 169 insertions(+), 70 deletions(-) diff --git a/javalanglib/src/main/scala/java/lang/ClassValue.scala b/javalanglib/src/main/scala/java/lang/ClassValue.scala index c471b78031..eae2eff71a 100644 --- a/javalanglib/src/main/scala/java/lang/ClassValue.scala +++ b/javalanglib/src/main/scala/java/lang/ClassValue.scala @@ -57,7 +57,7 @@ abstract class ClassValue[T] protected () { * then `get`. */ if (useJSMap) { - undefOrGetOrElseCompute(mapGet(jsMap, `type`)) { () => + undefOrGetOrElse(mapGet(jsMap, `type`)) { if (mapHas(jsMap, `type`)) { ().asInstanceOf[T] } else { diff --git a/javalanglib/src/main/scala/java/lang/Float.scala b/javalanglib/src/main/scala/java/lang/Float.scala index 523bd8ed1a..3bcbd5bcda 100644 --- a/javalanglib/src/main/scala/java/lang/Float.scala +++ b/javalanglib/src/main/scala/java/lang/Float.scala @@ -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) } diff --git a/javalanglib/src/main/scala/java/lang/Integer.scala b/javalanglib/src/main/scala/java/lang/Integer.scala index dbd6ba89ee..80001ceaf9 100644 --- a/javalanglib/src/main/scala/java/lang/Integer.scala +++ b/javalanglib/src/main/scala/java/lang/Integer.scala @@ -130,7 +130,7 @@ object Integer { decodeGeneric(nm, valueOf(_, _)) @inline private[lang] def decodeGeneric[A](nm: String, - parse: js.Function2[String, Int, A]): A = { + parse: (String, Int) => A): A = { val len = nm.length() var i = 0 diff --git a/javalanglib/src/main/scala/java/lang/StackTrace.scala b/javalanglib/src/main/scala/java/lang/StackTrace.scala index dbbd654842..26e0835ea0 100644 --- a/javalanglib/src/main/scala/java/lang/StackTrace.scala +++ b/javalanglib/src/main/scala/java/lang/StackTrace.scala @@ -426,7 +426,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)) @@ -471,7 +471,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/javalanglib/src/main/scala/java/lang/System.scala b/javalanglib/src/main/scala/java/lang/System.scala index 954f5ec050..216aac091c 100644 --- a/javalanglib/src/main/scala/java/lang/System.scala +++ b/javalanglib/src/main/scala/java/lang/System.scala @@ -70,7 +70,7 @@ object System { (new js.Date).getTime().toLong private object NanoTime { - val getHighPrecisionTime: js.Function0[scala.Double] = { + val getHighPrecisionTime: () => scala.Double = { import js.DynamicImplicits.truthValue if (js.typeOf(global.performance) != "undefined") { @@ -102,7 +102,7 @@ object System { def mismatch(): Nothing = throw new ArrayStoreException("Incompatible array types") - def impl(srcLen: Int, destLen: Int, f: js.Function2[Int, Int, Any]): Unit = { + def impl(srcLen: Int, destLen: Int, f: (Int, Int) => Any): Unit = { /* Perform dummy swaps to trigger an ArrayIndexOutOfBoundsException or * UBE if the positions / lengths are bad. */ diff --git a/javalanglib/src/main/scala/java/lang/Throwables.scala b/javalanglib/src/main/scala/java/lang/Throwables.scala index 3124009f1c..8eda9f7929 100644 --- a/javalanglib/src/main/scala/java/lang/Throwables.scala +++ b/javalanglib/src/main/scala/java/lang/Throwables.scala @@ -92,7 +92,7 @@ class Throwable protected (s: String, private var e: Throwable, def printStackTrace(s: java.io.PrintWriter): Unit = printStackTraceImpl(s.println(_)) - private[this] def printStackTraceImpl(sprintln: js.Function1[String, Unit]): Unit = { + private[this] def printStackTraceImpl(sprintln: String => Unit): Unit = { getStackTrace() // will init it if still null // Message diff --git a/javalanglib/src/main/scala/java/lang/Utils.scala b/javalanglib/src/main/scala/java/lang/Utils.scala index 5831f160d2..19652a9337 100644 --- a/javalanglib/src/main/scala/java/lang/Utils.scala +++ b/javalanglib/src/main/scala/java/lang/Utils.scala @@ -31,17 +31,12 @@ private[lang] object Utils { x.asInstanceOf[A] @inline - def undefOrGetOrElse[A](x: js.UndefOr[A], default: A): A = + def undefOrGetOrElse[A](x: js.UndefOr[A])(default: => A): A = if (undefOrIsDefined(x)) undefOrForceGet(x) else default @inline - def undefOrGetOrElseCompute[A](x: js.UndefOr[A])(default: js.Function0[A]): A = - if (undefOrIsDefined(x)) undefOrForceGet(x) - else default() - - @inline - def undefOrFold[A, B](x: js.UndefOr[A])(default: B, f: js.Function1[A, B]): B = + def undefOrFold[A, B](x: js.UndefOr[A])(default: => B, f: A => B): B = if (undefOrIsDefined(x)) f(undefOrForceGet(x)) else default @@ -123,7 +118,7 @@ private[lang] object Utils { map.asInstanceOf[MapRaw[K, V]].set(key, value) @inline - def forArrayElems[A](array: js.Array[A])(f: js.Function1[A, Any]): Unit = { + def forArrayElems[A](array: js.Array[A])(f: A => Any): Unit = { val len = array.length var i = 0 while (i != len) { diff --git a/javalanglib/src/main/scala/java/lang/_String.scala b/javalanglib/src/main/scala/java/lang/_String.scala index b9e30ac9f4..1cc1d7622a 100644 --- a/javalanglib/src/main/scala/java/lang/_String.scala +++ b/javalanglib/src/main/scala/java/lang/_String.scala @@ -652,9 +652,7 @@ for (cp <- 0 to Character.MAX_CODE_POINT) { * the character at the given index is not special. */ @inline - private def replaceCharsAtIndex( - replacementAtIndex: js.Function1[Int, String]): String = { - + private def replaceCharsAtIndex(replacementAtIndex: Int => String): String = { var prep = "" val len = this.length() var i = 0 @@ -787,7 +785,7 @@ for (cp <- 0 to Character.MAX_CODE_POINT) { def indent(n: Int): String = { - def forEachLn(f: js.Function1[String, String]): String = { + def forEachLn(f: String => String): String = { var out = "" var i = 0 val xs = splitLines() diff --git a/project/Build.scala b/project/Build.scala index 7ae2b163c8..ebf4aca8c5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -571,6 +571,9 @@ object Build { ) val cleanIRSettings = Def.settings( + // In order to rewrite anonymous functions, the code must not be specialized + scalacOptions += "-no-specialization", + products in Compile := { val s = streams.value diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 8c73a20194..ace182e25a 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -10,6 +10,7 @@ import java.io._ import java.net.URI import java.nio.file.Files +import scala.collection.immutable.IndexedSeq import scala.collection.mutable import sbt.{Logger, MessageOnlyException} @@ -31,6 +32,8 @@ import sbt.{Logger, MessageOnlyException} * the JS load spec of the mentioned class ref. * - Replace calls to "intrinsic" methods of the Scala.js library by their * meaning at call site. + * - Erase Scala FunctionN's to JavaScript closures (hence, of type `any`), + * including `new AnonFunctionN` and `someFunctionN.apply` calls. * * Afterwards, we check that the IR does not contain any reference to classes * under the `scala.*` package. @@ -157,6 +160,7 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { newMemberDefs, topLevelExportDefs)( optimizerHints)(pos) + // Only validate the hierarchy; do not transform validateClassName(preprocessedTree.name.name) for (superClass <- preprocessedTree.superClass) validateClassName(superClass.name) @@ -194,14 +198,26 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { override def transformMemberDef(memberDef: MemberDef): MemberDef = { super.transformMemberDef(memberDef) match { + case m @ FieldDef(flags, name, originalName, ftpe) => + implicit val pos = m.pos + FieldDef(flags, name, originalName, transformType(ftpe)) case m @ MethodDef(flags, name, originalName, args, resultType, body) => - MethodDef(flags, transformMethodIdent(name), originalName, args, - resultType, body)(m.optimizerHints, m.hash)(m.pos) + implicit val pos = m.pos + MethodDef(flags, transformMethodIdent(name), originalName, transformParamDefs(args), + transformType(resultType), body)(m.optimizerHints, m.hash) case m => m } } + private def transformParamDefs(paramDefs: List[ParamDef]): List[ParamDef] = { + for (paramDef <- paramDefs) yield { + implicit val pos = paramDef.pos + val ParamDef(name, originalName, ptpe, mutable) = paramDef + ParamDef(name, originalName, transformType(ptpe), mutable) + } + } + override def transform(tree: Tree, isStat: Boolean): Tree = { implicit val pos = tree.pos @@ -209,7 +225,8 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { val tree2 = if (tree1 eq tree) super.transform(tree, isStat) else transform(tree1, isStat) val result = postTransform(tree2, isStat) - validateType(result.tpe) + if (transformType(result.tpe) != result.tpe) + reportError(s"the result type of a ${result.getClass().getSimpleName()} was not transformed") result } @@ -218,6 +235,15 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { implicit val pos = tree.pos tree match { + // new AnonFunctionN(closure) --> closure + case New(AnonFunctionNClass(n), _, List(closure)) => + closure + + // someFunctionN.apply(args) --> someFunctionN(args) + case Apply(ApplyFlags.empty, fun, MethodIdent(FunctionApplyMethodName(n)), args) + if isFunctionNType(n, fun.tpe) => + JSFunctionApply(fun, args) + case IntrinsicCall(JSAnyMod, `jsAnyFromIntMethodName`, List(arg)) => arg case IntrinsicCall(JSAnyMod, `jsAnyFromStringMethodName`, List(arg)) => @@ -254,29 +280,45 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { implicit val pos = tree.pos tree match { + case VarDef(name, originalName, vtpe, mutable, rhs) => + VarDef(name, originalName, transformType(vtpe), mutable, rhs) + + case Labeled(label, tpe, body) => + Labeled(label, transformType(tpe), body) + case If(cond, thenp, elsep) => + If(cond, thenp, elsep)(transformType(tree.tpe)) + case TryCatch(block, errVar, errVarOriginalName, handler) => + TryCatch(block, errVar, errVarOriginalName, handler)(transformType(tree.tpe)) + case Match(selector, cases, default) => + Match(selector, cases, default)(transformType(tree.tpe)) + case New(className, ctor, args) => - New(className, transformMethodIdent(ctor), args) + New(transformNonJSClassName(className), transformMethodIdent(ctor), args) + case Select(qualifier, className, field) => + Select(qualifier, transformNonJSClassName(className), field)(transformType(tree.tpe)) case t: Apply => - Apply(t.flags, t.receiver, transformMethodIdent(t.method), - t.args)(t.tpe) + Apply(t.flags, t.receiver, transformMethodIdent(t.method), t.args)( + transformType(t.tpe)) case t: ApplyStatically => - validateNonJSClassName(t.className) - ApplyStatically(t.flags, t.receiver, t.className, - transformMethodIdent(t.method), t.args)(t.tpe) + ApplyStatically(t.flags, t.receiver, + transformNonJSClassName(t.className), + transformMethodIdent(t.method), t.args)(transformType(t.tpe)) case t: ApplyStatic => - validateNonJSClassName(t.className) - ApplyStatic(t.flags, t.className, - transformMethodIdent(t.method), t.args)(t.tpe) + ApplyStatic(t.flags, transformNonJSClassName(t.className), + transformMethodIdent(t.method), t.args)(transformType(t.tpe)) case NewArray(typeRef, lengths) => NewArray(transformArrayTypeRef(typeRef), lengths) case ArrayValue(typeRef, elems) => ArrayValue(transformArrayTypeRef(typeRef), elems) + case ArraySelect(array, index) => + ArraySelect(array, index)(transformType(tree.tpe)) - case t: IsInstanceOf => - validateType(t.testType) - t + case IsInstanceOf(expr, testType) => + IsInstanceOf(expr, transformType(testType)) + case AsInstanceOf(expr, tpe) => + AsInstanceOf(expr, transformType(tpe)) case LoadJSConstructor(className) => genLoadFromLoadSpecOf(className) @@ -288,6 +330,13 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { reportError(s"illegal ClassOf(${t.typeRef})") t + case t @ VarRef(ident) => + VarRef(ident)(transformType(t.tpe)) + + case Closure(arrow, captureParams, params, restParam, body, captureValues) => + Closure(arrow, transformParamDefs(captureParams), transformParamDefs(params), + restParam, body, captureValues) + case _ => tree } @@ -327,28 +376,15 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { private def transformMethodIdent(ident: MethodIdent): MethodIdent = { implicit val pos = ident.pos - val methodName = ident.name - val paramTypeRefs = methodName.paramTypeRefs - val newParamTypeRefs = paramTypeRefs.map(transformTypeRef) - val resultTypeRef = methodName.resultTypeRef - val newResultTypeRef = transformTypeRef(resultTypeRef) - if (newParamTypeRefs == paramTypeRefs && newResultTypeRef == resultTypeRef) { - ident - } else { - val newMethodName = MethodName(methodName.simpleName, - newParamTypeRefs, newResultTypeRef, methodName.isReflectiveProxy) - MethodIdent(newMethodName) - } + MethodIdent(transformMethodName(ident.name)) } private def transformClassRef(cls: ClassRef)( implicit pos: Position): ClassRef = { - if (jsTypes.contains(cls.className)) { + if (jsTypes.contains(cls.className)) ClassRef(ObjectClass) - } else { - validateClassName(cls.className) - cls - } + else + ClassRef(transformClassName(cls.className)) } private def transformArrayTypeRef(typeRef: ArrayTypeRef)( @@ -357,12 +393,10 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { case _: PrimRef => typeRef case ClassRef(baseClassName) => - if (jsTypes.contains(baseClassName)) { + if (jsTypes.contains(baseClassName)) ArrayTypeRef(ClassRef(ObjectClass), typeRef.dimensions) - } else { - validateClassName(baseClassName) - typeRef - } + else + ArrayTypeRef(ClassRef(transformClassName(baseClassName)), typeRef.dimensions) } } @@ -389,27 +423,47 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } } - private def validateType(tpe: Type)(implicit pos: Position): Unit = { + private def transformType(tpe: Type)(implicit pos: Position): Type = { tpe match { + case ClassType(ObjectClass) => + // In java.lang.Object iself, there are ClassType(ObjectClass) that must be preserved as is. + tpe case ClassType(cls) => - validateClassName(cls) - case ArrayType(ArrayTypeRef(ClassRef(cls), _)) => - validateClassName(cls) + transformClassName(cls) match { + case ObjectClass => AnyType + case newCls => ClassType(newCls) + } + case ArrayType(arrayTypeRef) => + ArrayType(transformArrayTypeRef(arrayTypeRef)) case _ => - // ok + tpe } } + private def transformClassName(cls: ClassName)(implicit pos: Position): ClassName = { + ClassNameSubstitutions.getOrElse(cls, { + validateClassName(cls) + cls + }) + } + private def validateClassName(cls: ClassName)(implicit pos: Position): Unit = { if (cls.nameString.startsWith("scala.")) reportError(s"Illegal reference to Scala class ${cls.nameString}") } - private def validateNonJSClassName(cls: ClassName)(implicit pos: Position): Unit = { - if (jsTypes.contains(cls)) + private def transformNonJSClassName(cls: ClassName)(implicit pos: Position): ClassName = { + if (jsTypes.contains(cls)) { reportError(s"Invalid reference to JS class ${cls.nameString}") - else - validateClassName(cls) + cls + } else { + transformClassName(cls) + } + } + + private def transformMethodName(name: MethodName)(implicit pos: Position): MethodName = { + MethodName(name.simpleName, name.paramTypeRefs.map(transformTypeRef), + transformTypeRef(name.resultTypeRef), name.isReflectiveProxy) } private def reportError(msg: String)(implicit pos: Position): Unit = { @@ -419,6 +473,8 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } object JavalibIRCleaner { + private final val MaxFunctionArity = 4 + private val JavaIOSerializable = ClassName("java.io.Serializable") private val JSAny = ClassName("scala.scalajs.js.Any") private val JSAnyMod = ClassName("scala.scalajs.js.Any$") @@ -430,6 +486,12 @@ object JavalibIRCleaner { private val JSStringOpsMod = ClassName("scala.scalajs.js.JSStringOps$") private val ScalaSerializable = ClassName("scala.Serializable") + private val FunctionNClasses: IndexedSeq[ClassName] = + (0 to MaxFunctionArity).map(n => ClassName(s"scala.Function$n")) + + private val AnonFunctionNClasses: IndexedSeq[ClassName] = + (0 to MaxFunctionArity).map(n => ClassName(s"scala.scalajs.runtime.AnonFunction$n")) + private val enableJSNumberOpsDoubleMethodName = MethodName("enableJSNumberOps", List(DoubleRef), ClassRef(JSNumberOps)) private val enableJSNumberOpsIntMethodName = @@ -446,4 +508,45 @@ object JavalibIRCleaner { MethodName("truthValue", List(ClassRef(JSDynamic)), BooleanRef) private val writeReplaceMethodName = MethodName("writeReplace", Nil, ClassRef(ObjectClass)) + + private val functionApplyMethodNames: IndexedSeq[MethodName] = { + (0 to MaxFunctionArity).map { n => + MethodName("apply", (1 to n).toList.map(_ => ClassRef(ObjectClass)), ClassRef(ObjectClass)) + } + } + + private object AnonFunctionNClass { + private val AnonFunctionNClassToN: Map[ClassName, Int] = + AnonFunctionNClasses.zipWithIndex.toMap + + def apply(n: Int): ClassName = AnonFunctionNClasses(n) + + def unapply(cls: ClassName): Option[Int] = AnonFunctionNClassToN.get(cls) + } + + private object FunctionApplyMethodName { + private val FunctionApplyMethodNameToN: Map[MethodName, Int] = + functionApplyMethodNames.zipWithIndex.toMap + + def apply(n: Int): MethodName = functionApplyMethodNames(n) + + def unapply(name: MethodName): Option[Int] = FunctionApplyMethodNameToN.get(name) + } + + private def isFunctionNType(n: Int, tpe: Type): Boolean = tpe match { + case ClassType(cls) => + cls == FunctionNClasses(n) || cls == AnonFunctionNClasses(n) + case _ => + false + } + + private val ClassNameSubstitutions: Map[ClassName, ClassName] = { + val functionTypePairs = for { + funClass <- FunctionNClasses ++ AnonFunctionNClasses + } yield { + funClass -> ObjectClass + } + + functionTypePairs.toMap + } } From ede0e84f566c4da2be76d8dd56f71c2a4bd5f50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 3 Jun 2022 16:10:33 +0200 Subject: [PATCH 06/75] Rewrite Scala 2.11 string interpolators to string concatenation. In Scala 2.12+, the compiler already performs that rewrite. This allows to use string interpolators in a number of places of the javalanglib, where it makes most sense (notably for user-facing error messages). --- .../src/main/scala/java/lang/Byte.scala | 2 +- .../src/main/scala/java/lang/Double.scala | 2 +- .../src/main/scala/java/lang/Float.scala | 4 +- .../src/main/scala/java/lang/Integer.scala | 2 +- .../src/main/scala/java/lang/Long.scala | 2 +- .../src/main/scala/java/lang/Short.scala | 2 +- .../src/main/scala/java/lang/StackTrace.scala | 4 +- .../src/main/scala/java/lang/System.scala | 2 +- .../src/main/scala/java/lang/Throwables.scala | 18 +++---- .../src/main/scala/java/lang/_String.scala | 2 +- project/JavalibIRCleaner.scala | 50 +++++++++++++++++++ 11 files changed, 70 insertions(+), 20 deletions(-) diff --git a/javalanglib/src/main/scala/java/lang/Byte.scala b/javalanglib/src/main/scala/java/lang/Byte.scala index 2c563b9e73..ef2287af35 100644 --- a/javalanglib/src/main/scala/java/lang/Byte.scala +++ b/javalanglib/src/main/scala/java/lang/Byte.scala @@ -83,7 +83,7 @@ object Byte { def parseByte(s: String, radix: Int): scala.Byte = { val r = Integer.parseInt(s, radix) if (r < MIN_VALUE || r > MAX_VALUE) - throw new NumberFormatException("For input string: \"" + s + "\"") + throw new NumberFormatException(s"""For input string: "$s"""") else r.toByte } diff --git a/javalanglib/src/main/scala/java/lang/Double.scala b/javalanglib/src/main/scala/java/lang/Double.scala index 094cfb8315..bb6626981e 100644 --- a/javalanglib/src/main/scala/java/lang/Double.scala +++ b/javalanglib/src/main/scala/java/lang/Double.scala @@ -116,7 +116,7 @@ object Double { // Slow path of `parseDouble` for hexadecimal notation and failure private def parseDoubleSlowPath(s: String): scala.Double = { def fail(): Nothing = - throw new NumberFormatException("For input string: \"" + s + "\"") + throw new NumberFormatException(s"""For input string: "$s"""") val groups = doubleStrHexPat.exec(s) if (groups == null) diff --git a/javalanglib/src/main/scala/java/lang/Float.scala b/javalanglib/src/main/scala/java/lang/Float.scala index 3bcbd5bcda..4dcd15a6c1 100644 --- a/javalanglib/src/main/scala/java/lang/Float.scala +++ b/javalanglib/src/main/scala/java/lang/Float.scala @@ -113,7 +113,7 @@ object Float { val groups = parseFloatRegExp.exec(s) if (groups == null) - throw new NumberFormatException("For input string: \"" + s + "\"") + throw new NumberFormatException(s"""For input string: "$s"""") val absResult = if (undefOrIsDefined(groups(2))) { scala.Float.NaN @@ -264,7 +264,7 @@ object Float { * subnormal floats). */ if (biasedK == 0) - throw new AssertionError("parseFloatCorrection was given a subnormal mid: " + mid) + throw new AssertionError(s"parseFloatCorrection was given a subnormal mid: $mid") val mExplicitBits = midBits & ((1L << mbits) - 1) val mImplicit1Bit = 1L << mbits // the implicit '1' bit of a normalized floating-point number diff --git a/javalanglib/src/main/scala/java/lang/Integer.scala b/javalanglib/src/main/scala/java/lang/Integer.scala index 80001ceaf9..ec8c034064 100644 --- a/javalanglib/src/main/scala/java/lang/Integer.scala +++ b/javalanglib/src/main/scala/java/lang/Integer.scala @@ -84,7 +84,7 @@ object Integer { signed: scala.Boolean): scala.Int = { def fail(): Nothing = - throw new NumberFormatException("For input string: \"" + s + "\"") + throw new NumberFormatException(s"""For input string: "$s"""") val len = if (s == null) 0 else s.length diff --git a/javalanglib/src/main/scala/java/lang/Long.scala b/javalanglib/src/main/scala/java/lang/Long.scala index cd08a8c839..e6bf36ac1c 100644 --- a/javalanglib/src/main/scala/java/lang/Long.scala +++ b/javalanglib/src/main/scala/java/lang/Long.scala @@ -318,7 +318,7 @@ object Long { } private def parseLongError(s: String): Nothing = - throw new NumberFormatException("For input string: \"" + s + "\"") + throw new NumberFormatException(s"""For input string: "$s"""") @inline def `new`(value: scala.Long): Long = valueOf(value) diff --git a/javalanglib/src/main/scala/java/lang/Short.scala b/javalanglib/src/main/scala/java/lang/Short.scala index f44729b500..12149680ef 100644 --- a/javalanglib/src/main/scala/java/lang/Short.scala +++ b/javalanglib/src/main/scala/java/lang/Short.scala @@ -82,7 +82,7 @@ object Short { def parseShort(s: String, radix: Int): scala.Short = { val r = Integer.parseInt(s, radix) if (r < MIN_VALUE || r > MAX_VALUE) - throw new NumberFormatException("For input string: \"" + s + "\"") + throw new NumberFormatException(s"""For input string: "$s"""") else r.toShort } diff --git a/javalanglib/src/main/scala/java/lang/StackTrace.scala b/javalanglib/src/main/scala/java/lang/StackTrace.scala index 26e0835ea0..66c3a6dac8 100644 --- a/javalanglib/src/main/scala/java/lang/StackTrace.scala +++ b/javalanglib/src/main/scala/java/lang/StackTrace.scala @@ -250,8 +250,8 @@ private[lang] object StackTrace { var index = 0 while (index <= 22) { if (index >= 2) - dictSet(dict, "T" + index, "scala_Tuple" + index) - dictSet(dict, "F" + index, "scala_Function" + index) + dictSet(dict, s"T$index", s"scala_Tuple$index") + dictSet(dict, s"F$index", s"scala_Function$index") index += 1 } diff --git a/javalanglib/src/main/scala/java/lang/System.scala b/javalanglib/src/main/scala/java/lang/System.scala index 216aac091c..5b34a6680c 100644 --- a/javalanglib/src/main/scala/java/lang/System.scala +++ b/javalanglib/src/main/scala/java/lang/System.scala @@ -349,7 +349,7 @@ private final class JSConsoleBasedPrintStream(isErr: scala.Boolean) // This is the method invoked by Predef.println(x). @inline - override def println(obj: AnyRef): Unit = printString("" + obj + "\n") + override def println(obj: AnyRef): Unit = printString(s"$obj\n") private def printString(s: String): Unit = { var rest: String = s diff --git a/javalanglib/src/main/scala/java/lang/Throwables.scala b/javalanglib/src/main/scala/java/lang/Throwables.scala index 8eda9f7929..87484ff2ec 100644 --- a/javalanglib/src/main/scala/java/lang/Throwables.scala +++ b/javalanglib/src/main/scala/java/lang/Throwables.scala @@ -102,7 +102,7 @@ class Throwable protected (s: String, private var e: Throwable, if (stackTrace.length != 0) { var i = 0 while (i < stackTrace.length) { - sprintln(" at "+stackTrace(i)) + sprintln(s" at ${stackTrace(i)}") i += 1 } } else { @@ -119,7 +119,7 @@ class Throwable protected (s: String, private var e: Throwable, val thisLength = thisTrace.length val parentLength = parentTrace.length - sprintln("Caused by: " + wCause.toString) + sprintln(s"Caused by: $wCause") if (thisLength != 0) { /* Count how many frames are shared between this stack trace and the @@ -141,12 +141,12 @@ class Throwable protected (s: String, private var e: Throwable, val lengthToPrint = thisLength - sameFrameCount var i = 0 while (i < lengthToPrint) { - sprintln(" at "+thisTrace(i)) + sprintln(s" at ${thisTrace(i)}") i += 1 } if (sameFrameCount > 0) - sprintln(" ... " + sameFrameCount + " more") + sprintln(s" ... $sameFrameCount more") } else { sprintln(" ") } @@ -161,7 +161,7 @@ class Throwable protected (s: String, private var e: Throwable, val className = getClass().getName() val message = getMessage() if (message eq null) className - else className + ": " + message + else s"$className: $message" } def addSuppressed(exception: Throwable): Unit = { @@ -346,7 +346,7 @@ class ArithmeticException(s: String) extends RuntimeException(s) { } class ArrayIndexOutOfBoundsException(s: String) extends IndexOutOfBoundsException(s) { - def this(index: Int) = this("Array index out of range: " + index) + def this(index: Int) = this(s"Array index out of range: $index") def this() = this(null) } @@ -370,7 +370,7 @@ class CloneNotSupportedException(s: String) extends Exception(s) { } class EnumConstantNotPresentException(e: Class[_ <: Enum[_]], c: String) - extends RuntimeException(e.getName() + "." + c) { + extends RuntimeException(s"${e.getName()}.$c") { def enumType(): Class[_ <: Enum[_]] = e def constantName(): String = c } @@ -468,12 +468,12 @@ class SecurityException(s: String, e: Throwable) extends RuntimeException(s, e) } class StringIndexOutOfBoundsException(s: String) extends IndexOutOfBoundsException(s) { - def this(index: Int) = this("String index out of range: " + index) + def this(index: Int) = this(s"String index out of range: $index") def this() = this(null) } class TypeNotPresentException(t: String, e: Throwable) - extends RuntimeException("Type " + t + " not present", e) { + extends RuntimeException(s"Type $t not present", e) { def typeName(): String = t } diff --git a/javalanglib/src/main/scala/java/lang/_String.scala b/javalanglib/src/main/scala/java/lang/_String.scala index 1cc1d7622a..71bd4c3c59 100644 --- a/javalanglib/src/main/scala/java/lang/_String.scala +++ b/javalanglib/src/main/scala/java/lang/_String.scala @@ -906,7 +906,7 @@ for (cp <- 0 to Character.MAX_CODE_POINT) { result += codePoint.toChar // bad escape otherwise, this catches everything else including the Unicode ones case bad => - throw new IllegalArgumentException("Illegal escape: `\\" + bad + "`") + throw new IllegalArgumentException(s"Illegal escape: `\\$bad`") } // skip ahead 2 chars (\ and the escape char) at minimum, cases above can add more if needed i += 2 diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index ace182e25a..70d9a046fd 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -262,6 +262,33 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { JSUnaryOp(JSUnaryOp.!, JSUnaryOp(JSUnaryOp.!, arg)), BooleanType) + // 2.11 s"..." interpolator + case Apply( + ApplyFlags.empty, + New(StringContextClass, MethodIdent(`stringContextCtorMethodName`), + List(ScalaVarArgsReadOnlyLiteral(stringElems))), + MethodIdent(`sMethodName`), + List(ScalaVarArgsReadOnlyLiteral(valueElems))) => + if (stringElems.size != valueElems.size + 1) { + reportError("Found s\"...\" interpolator but the sizes do not match") + tree + } else { + val processedEscapesStringElems = stringElems.map { s => + (s: @unchecked) match { + case StringLiteral(value) => + StringLiteral(StringContext.processEscapes(value)) + } + } + val stringsIter = processedEscapesStringElems.iterator + val valuesIter = valueElems.iterator + var result: Tree = stringsIter.next() + while (valuesIter.hasNext) { + result = BinaryOp(BinaryOp.String_+, result, valuesIter.next()) + result = BinaryOp(BinaryOp.String_+, result, stringsIter.next()) + } + result + } + case _ => tree } @@ -276,6 +303,19 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } } + private object ScalaVarArgsReadOnlyLiteral { + def unapply(tree: Apply): Option[List[Tree]] = tree match { + case IntrinsicCall(ScalaJSRuntimeMod, `toScalaVarArgsReadOnlyMethodName`, + List(JSArrayConstr(args))) => + if (args.forall(_.isInstanceOf[Tree])) + Some(args.map(_.asInstanceOf[Tree])) + else + None + case _ => + None + } + } + private def postTransform(tree: Tree, isStat: Boolean): Tree = { implicit val pos = tree.pos @@ -478,13 +518,17 @@ object JavalibIRCleaner { private val JavaIOSerializable = ClassName("java.io.Serializable") private val JSAny = ClassName("scala.scalajs.js.Any") private val JSAnyMod = ClassName("scala.scalajs.js.Any$") + private val JSArray = ClassName("scala.scalajs.js.Array") private val JSDynamic = ClassName("scala.scalajs.js.Dynamic") private val JSDynamicImplicitsMod = ClassName("scala.scalajs.js.DynamicImplicits$") private val JSNumberOps = ClassName("scala.scalajs.js.JSNumberOps") private val JSNumberOpsMod = ClassName("scala.scalajs.js.JSNumberOps$") private val JSStringOps = ClassName("scala.scalajs.js.JSStringOps") private val JSStringOpsMod = ClassName("scala.scalajs.js.JSStringOps$") + private val ReadOnlySeq = ClassName("scala.collection.Seq") private val ScalaSerializable = ClassName("scala.Serializable") + private val ScalaJSRuntimeMod = ClassName("scala.scalajs.runtime.package$") + private val StringContextClass = ClassName("scala.StringContext") private val FunctionNClasses: IndexedSeq[ClassName] = (0 to MaxFunctionArity).map(n => ClassName(s"scala.Function$n")) @@ -504,6 +548,12 @@ object JavalibIRCleaner { MethodName("fromString", List(ClassRef(BoxedStringClass)), ClassRef(JSAny)) private val number2dynamicMethodName = MethodName("number2dynamic", List(DoubleRef), ClassRef(JSDynamic)) + private val sMethodName = + MethodName("s", List(ClassRef(ReadOnlySeq)), ClassRef(BoxedStringClass)) + private val stringContextCtorMethodName = + MethodName.constructor(List(ClassRef(ReadOnlySeq))) + private val toScalaVarArgsReadOnlyMethodName = + MethodName("toScalaVarArgs", List(ClassRef(JSArray)), ClassRef(ReadOnlySeq)) private val truthValueMethodName = MethodName("truthValue", List(ClassRef(JSDynamic)), BooleanRef) private val writeReplaceMethodName = From fa0009195b574aa39c2fa516fe97b0906eb6d214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 29 Jun 2022 17:17:08 +0200 Subject: [PATCH 07/75] Bump the minor version to 1.11.0 for the upcoming changes. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c45c54b3e2..3da2a342aa 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.10.2-SNAPSHOT", + current = "1.11.0-SNAPSHOT", binaryEmitted = "1.8" ) From e8e662a53b99bed0848bd1e8a3c2a3cde28443d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 29 Jun 2022 17:14:03 +0200 Subject: [PATCH 08/75] Optimize `getOrElse{,Update}` for `js.Dictionary` and `js.Map`. The default implementations go through `get`, which allocates a `Some` instance. This improvement causes `ju.regex.PatternCompiler` not to reach `Some`, `None` and `Option` anymore, which dramatically reduces its test size, although that improvement will not be seen in actual codebases that reach `Option` types anywhere else. This is a forward binary incompatible change, because the overrides in `js.WrappedDictionary` have a more precise binary signature (taking a `String` for the `key` instead of an `Object` in the erased superclass). --- .../scala/scalajs/js/WrappedDictionary.scala | 17 +++++++++++++ .../scala/scalajs/js/WrappedMap.scala | 25 +++++++++++++++++-- .../scala/scalajs/js/WrappedDictionary.scala | 17 +++++++++++++ .../scala/scalajs/js/WrappedMap.scala | 25 +++++++++++++++++-- .../org/scalajs/linker/LibrarySizeTest.scala | 6 ++--- 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala index 05e3875283..a7624b3e46 100644 --- a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala +++ b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala @@ -54,6 +54,23 @@ final class WrappedDictionary[A](private val dict: js.Dictionary[A]) throw new NoSuchElementException("key not found: " + key) } + override def getOrElse[V1 >: A](key: String, default: => V1): V1 = { + if (contains(key)) + rawApply(key) + else + default + } + + override def getOrElseUpdate(key: String, op: => A): A = { + if (contains(key)) { + rawApply(key) + } else { + val v = op + update(key, v) + v + } + } + @inline private def rawApply(key: String): A = dict.asInstanceOf[DictionaryRawApply[A]].rawApply(key) diff --git a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala index d3c009440f..04d2f08517 100644 --- a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala +++ b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala @@ -40,18 +40,39 @@ final class WrappedMap[K, V](private val underlying: js.Map[K, V]) def get(key: K): Option[V] = { if (contains(key)) - Some(underlying.asInstanceOf[js.Map.Raw[K, V]].get(key)) + Some(rawApply(key)) else None } override def apply(key: K): V = { if (contains(key)) - underlying.asInstanceOf[js.Map.Raw[K, V]].get(key) + rawApply(key) else throw new NoSuchElementException("key not found: " + key) } + override def getOrElse[V1 >: V](key: K, default: => V1): V1 = { + if (contains(key)) + rawApply(key) + else + default + } + + override def getOrElseUpdate(key: K, op: => V): V = { + if (contains(key)) { + rawApply(key) + } else { + val v = op + update(key, v) + v + } + } + + @inline + private def rawApply(key: K): V = + underlying.asInstanceOf[js.Map.Raw[K, V]].get(key) + override def size: Int = underlying.size diff --git a/library/src/main/scala-old-collections/scala/scalajs/js/WrappedDictionary.scala b/library/src/main/scala-old-collections/scala/scalajs/js/WrappedDictionary.scala index 9ff5be6ebb..6ea34028ad 100644 --- a/library/src/main/scala-old-collections/scala/scalajs/js/WrappedDictionary.scala +++ b/library/src/main/scala-old-collections/scala/scalajs/js/WrappedDictionary.scala @@ -45,6 +45,23 @@ final class WrappedDictionary[A](private val dict: js.Dictionary[A]) throw new NoSuchElementException("key not found: " + key) } + override def getOrElse[V1 >: A](key: String, default: => V1): V1 = { + if (contains(key)) + rawApply(key) + else + default + } + + override def getOrElseUpdate(key: String, op: => A): A = { + if (contains(key)) { + rawApply(key) + } else { + val v = op + update(key, v) + v + } + } + @inline private def rawApply(key: String): A = dict.asInstanceOf[DictionaryRawApply[A]].rawApply(key) diff --git a/library/src/main/scala-old-collections/scala/scalajs/js/WrappedMap.scala b/library/src/main/scala-old-collections/scala/scalajs/js/WrappedMap.scala index 0544d2a3e5..78d39208d4 100644 --- a/library/src/main/scala-old-collections/scala/scalajs/js/WrappedMap.scala +++ b/library/src/main/scala-old-collections/scala/scalajs/js/WrappedMap.scala @@ -28,18 +28,39 @@ final class WrappedMap[K, V](private val underlying: js.Map[K, V]) def get(key: K): Option[V] = { if (contains(key)) - Some(underlying.asInstanceOf[js.Map.Raw[K, V]].get(key)) + Some(rawApply(key)) else None } override def apply(key: K): V = { if (contains(key)) - underlying.asInstanceOf[js.Map.Raw[K, V]].get(key) + rawApply(key) else throw new NoSuchElementException("key not found: " + key) } + override def getOrElse[V1 >: V](key: K, default: => V1): V1 = { + if (contains(key)) + rawApply(key) + else + default + } + + override def getOrElseUpdate(key: K, op: => V): V = { + if (contains(key)) { + rawApply(key) + } else { + val v = op + update(key, v) + v + } + } + + @inline + private def rawApply(key: K): V = + underlying.asInstanceOf[js.Map.Raw[K, V]].get(key) + override def size: Int = underlying.size diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 4df775208f..8e955e5b8d 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 183410, - expectedFullLinkSizeWithoutClosure = 171474, - expectedFullLinkSizeWithClosure = 30958, + expectedFastLinkSize = 146336, + expectedFullLinkSizeWithoutClosure = 135798, + expectedFullLinkSizeWithClosure = 22074, classDefs, moduleInitializers = MainTestModuleInitializers ) From 408a5ba83cdd6bd931d9e6e2b96976bee03810c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 29 Jun 2022 20:25:44 +0200 Subject: [PATCH 09/75] Bump the IR version to 1.11 for the upcoming changes. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3da2a342aa..0d63dfa3ec 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -18,7 +18,7 @@ import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( current = "1.11.0-SNAPSHOT", - binaryEmitted = "1.8" + binaryEmitted = "1.11-SNAPSHOT" ) /** Helper class to allow for testing of logic. */ From b7e7c56be51e993abc387f14f1006c4bd64d4a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 30 Jun 2022 20:19:23 +0200 Subject: [PATCH 10/75] Fix #4688: Introduce IR nodes to wrap/unwrap JavaScript exceptions. This allows to defer to linking the reference to `scala.scalajs.js.JavaScriptException`. For compatibility reasons, we must preserve that very class when it is available on the classpath. However, if it is not already on the classpath, the emitter provides an implementation through the linker-private-library. That implementation does not depend on the scalalib, as checked by the IR cleaner. We add a linker test in `RunTest` to make sure that we can indeed correctly link a codebase containing a `WrapAsThrowable` node, but not any implementation of `JavaScriptException`. In a hypothetical Scala.js 2.x, we would move `JavaScriptException` to the `java.lang` package. --- .../org/scalajs/nscplugin/GenJSCode.scala | 17 ++--- .../org/scalajs/nscplugin/JSDefinitions.scala | 2 - .../nscplugin/test/OptimizationTest.scala | 9 ++- .../main/scala/org/scalajs/ir/Hashers.scala | 8 +++ .../src/main/scala/org/scalajs/ir/Names.scala | 7 ++ .../main/scala/org/scalajs/ir/Printers.scala | 10 +++ .../scala/org/scalajs/ir/Serializers.scala | 13 ++++ .../src/main/scala/org/scalajs/ir/Tags.scala | 5 ++ .../scala/org/scalajs/ir/Transformers.scala | 6 ++ .../scala/org/scalajs/ir/Traversers.scala | 6 ++ .../src/main/scala/org/scalajs/ir/Trees.scala | 10 +++ .../scala/org/scalajs/ir/PrintersTest.scala | 9 +++ .../scala/scala/scalajs/runtime/package.scala | 2 + .../scalajs/js/JavaScriptException.scala | 29 ++++++++ .../backend/emitter/PrivateLibHolder.scala | 15 +++-- .../scala/org/scalajs/linker/RunTest.scala | 33 +++++++++ .../org/scalajs/linker/analyzer/Infos.scala | 16 +++++ .../linker/backend/emitter/EmitterNames.scala | 8 ++- .../backend/emitter/FunctionEmitter.scala | 48 +++++++++++++ .../linker/checker/ClassDefChecker.scala | 6 ++ .../scalajs/linker/checker/IRChecker.scala | 6 ++ .../frontend/optimizer/OptimizerCore.scala | 67 +++++++++++++++++-- project/Build.scala | 6 +- project/JavalibIRCleaner.scala | 8 ++- .../testsuite/compiler/OptimizerTest.scala | 20 ++++++ .../library/JavaScriptExceptionTest.scala | 17 +++++ 26 files changed, 347 insertions(+), 36 deletions(-) create mode 100644 linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 243c825c2c..3af61fbb70 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2398,14 +2398,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case Throw(expr) => val ex = genExpr(expr) js.Throw { - if (!ex.isInstanceOf[js.Null] && isMaybeJavaScriptException(expr.tpe)) { - genApplyMethod( - genLoadModule(RuntimePackageModule), - Runtime_unwrapJavaScriptException, - List(ex)) - } else { + if (!ex.isInstanceOf[js.Null] && isMaybeJavaScriptException(expr.tpe)) + js.UnwrapFromThrowable(ex) + else ex - } } /* !!! Copy-pasted from `CleanUp.scala` upstream and simplified with @@ -2930,12 +2926,7 @@ 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, { - genApplyMethod( - genLoadModule(RuntimePackageModule), - Runtime_wrapJavaScriptException, - List(origExceptVar)) - }) + encodeClassType(ThrowableClass), mutable = false, js.WrapAsThrowable(origExceptVar)) (valDef, valDef.ref) } else { (js.Skip(), origExceptVar) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala index c0423f9485..1724270ea4 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala @@ -110,8 +110,6 @@ trait JSDefinitions { lazy val Special_debugger = getMemberMethod(SpecialPackageModule, newTermName("debugger")) lazy val RuntimePackageModule = getPackageObject("scala.scalajs.runtime") - lazy val Runtime_wrapJavaScriptException = getMemberMethod(RuntimePackageModule, newTermName("wrapJavaScriptException")) - lazy val Runtime_unwrapJavaScriptException = getMemberMethod(RuntimePackageModule, newTermName("unwrapJavaScriptException")) lazy val Runtime_toScalaVarArgs = getMemberMethod(RuntimePackageModule, newTermName("toScalaVarArgs")) lazy val Runtime_toJSVarArgs = getMemberMethod(RuntimePackageModule, newTermName("toJSVarArgs")) lazy val Runtime_constructorOf = getMemberMethod(RuntimePackageModule, newTermName("constructorOf")) 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 095083f7fa..227eafde4d 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala @@ -517,8 +517,8 @@ class OptimizationTest extends JSASTTest { } } } - """.hasNot("call to the scala.scalajs.runtime.package$ package module class") { - case js.LoadModule(ScalaJSRuntimePackageClass) => + """.hasNot("WrapAsThrowable") { + case js.WrapAsThrowable(_) => } // Confidence check @@ -534,8 +534,8 @@ class OptimizationTest extends JSASTTest { } } } - """.hasExactly(1, "call to the scala.scalajs.runtime.package$ package module class") { - case js.LoadModule(ScalaJSRuntimePackageClass) => + """.hasExactly(1, "WrapAsThrowable") { + case js.WrapAsThrowable(_) => } } } @@ -543,7 +543,6 @@ class OptimizationTest extends JSASTTest { object OptimizationTest { private val ArrayModuleClass = ClassName("scala.Array$") - private val ScalaJSRuntimePackageClass = ClassName("scala.scalajs.runtime.package$") private val applySimpleMethodName = SimpleMethodName("apply") 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 06f75eb392..4f7ad70c1f 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -345,6 +345,14 @@ object Hashers { 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) 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 004db0c98b..d05ebc5ad7 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Names.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Names.scala @@ -486,6 +486,13 @@ object Names { /** `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") 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 737d3189b4..4e3b0b6287 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -558,6 +558,16 @@ object Printers { print(expr) print(')') + case WrapAsThrowable(expr) => + print("(") + print(expr) + print(")") + + case UnwrapFromThrowable(expr) => + print("(") + print(expr) + print(")") + // JavaScript expressions case JSNew(ctor, args) => 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 a8a1e13bb9..ef2bc10ab0 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -412,6 +412,14 @@ object Serializers { 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) @@ -1142,6 +1150,11 @@ object Serializers { 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(), readClassName(), readFieldIdent()) case TagJSSelect => JSSelect(readTree(), readTree()) 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 2ea3eac68f..b4efac66d9 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala @@ -122,6 +122,11 @@ private[ir] object Tags { final val TagJSNewTarget = TagJSImportMeta + 1 + // New in 1.11 + + final val TagWrapAsThrowable = TagJSNewTarget + 1 + final val TagUnwrapFromThrowable = TagWrapAsThrowable + 1 + // Tags for member defs final val TagFieldDef = 1 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 bd45d5d60f..f6629f98ed 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -149,6 +149,12 @@ object Transformers { case IdentityHashCode(expr) => IdentityHashCode(transformExpr(expr)) + case WrapAsThrowable(expr) => + WrapAsThrowable(transformExpr(expr)) + + case UnwrapFromThrowable(expr) => + UnwrapFromThrowable(transformExpr(expr)) + // JavaScript expressions case JSNew(ctor, args) => 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 d0bac2ffc1..8370736afb 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -142,6 +142,12 @@ object Traversers { case IdentityHashCode(expr) => traverse(expr) + case WrapAsThrowable(expr) => + traverse(expr) + + case UnwrapFromThrowable(expr) => + traverse(expr) + // JavaScript expressions case JSNew(ctor, args) => 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 8c8979ef35..a8b457be3d 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -493,6 +493,16 @@ object Trees { val tpe = IntType } + sealed case class WrapAsThrowable(expr: Tree)(implicit val pos: Position) + extends Tree { + val tpe = ClassType(ThrowableClass) + } + + 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])( 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 6135e16e87..bc90390910 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -599,6 +599,15 @@ class PrintersTest { 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)))) + } + @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)))) diff --git a/library/src/main/scala/scala/scalajs/runtime/package.scala b/library/src/main/scala/scala/scalajs/runtime/package.scala index f7622e5bc8..7aff7c9696 100644 --- a/library/src/main/scala/scala/scalajs/runtime/package.scala +++ b/library/src/main/scala/scala/scalajs/runtime/package.scala @@ -18,11 +18,13 @@ package object runtime { import scala.scalajs.runtime.Compat._ + @deprecated("Unused by the codegen", "1.11.0") def wrapJavaScriptException(e: Any): Throwable = e match { case e: Throwable => e case _ => js.JavaScriptException(e) } + @deprecated("Unused by the codegen", "1.11.0") def unwrapJavaScriptException(th: Throwable): Any = th match { case js.JavaScriptException(e) => e case _ => th diff --git a/linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala b/linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala new file mode 100644 index 0000000000..609a2dedfa --- /dev/null +++ b/linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala @@ -0,0 +1,29 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.scalajs.js + +import scala.language.reflectiveCalls + +final class JavaScriptException(val exception: scala.Any) + extends RuntimeException { + + override def getMessage(): String = exception.toString() + + override def fillInStackTrace(): Throwable = { + type JSExceptionEx = JavaScriptException { + def setStackTraceStateInternal(e: scala.Any): Unit + } + this.asInstanceOf[JSExceptionEx].setStackTraceStateInternal(exception) + this + } +} diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala index c671f94bfe..c39e9c566e 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala @@ -18,17 +18,18 @@ import org.scalajs.linker.interface.IRFile import org.scalajs.linker.standard.MemIRFileImpl object PrivateLibHolder { - private val relativeDir = "org/scalajs/linker/runtime/" - private val sjsirNames = Seq( - "RuntimeLong.sjsir", - "RuntimeLong$.sjsir", - "UndefinedBehaviorError.sjsir" + private val sjsirPaths = Seq( + "org/scalajs/linker/runtime/RuntimeLong.sjsir", + "org/scalajs/linker/runtime/RuntimeLong$.sjsir", + "org/scalajs/linker/runtime/UndefinedBehaviorError.sjsir", + "scala/scalajs/js/JavaScriptException.sjsir" ) val files: Seq[IRFile] = { - for (name <- sjsirNames) yield { + for (path <- sjsirPaths) yield { + val name = path.substring(path.lastIndexOf('/') + 1) new MemIRFileImpl( - path = relativeDir + name, + path = path, version = Some(""), // this indicates that the file never changes content = readResource(name) ) diff --git a/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala b/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala index ffac7fd344..7a4088a616 100644 --- a/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala +++ b/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala @@ -21,7 +21,9 @@ import org.junit.{Rule, Test} import org.junit.Assert._ import org.junit.rules.TemporaryFolder +import org.scalajs.ir.Names._ import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ import org.scalajs.junit.async._ @@ -58,6 +60,37 @@ class RunTest { TestKit.InputKind.ESModule) } + @Test + def wrapAsThrowable(): AsyncResult = await { + // Check that WrapAsThrowable can link without js.JavaScriptException on the classpath + + val getMessage = MethodName("getMessage", Nil, T) + + val e = VarRef("e")(ClassType(ThrowableClass)) + + val classDefs = Seq( + mainTestClassDef(Block( + VarDef("e", NON, ClassType(ThrowableClass), mutable = false, + WrapAsThrowable(JSNew(JSGlobalRef("RangeError"), List(str("boom"))))), + genAssert(IsInstanceOf(e, ClassType("java.lang.Exception"))), + genAssertEquals(str("RangeError: boom"), Apply(EAF, e, getMessage, Nil)(ClassType(BoxedStringClass))) + )) + ) + + testLinkAndRun(classDefs, MainTestModuleInitializers, StandardConfig(), + TestKit.InputKind.Script) + } + + private def genAssertEquals(expected: Tree, actual: Tree): Tree = + genAssert(BinaryOp(BinaryOp.===, expected, actual)) + + private def genAssert(test: Tree): Tree = { + If(UnaryOp(UnaryOp.Boolean_!, test), + Throw(JSNew(JSGlobalRef("Error"), List(str("Assertion failed")))), + Skip())( + NoType) + } + private def testLinkAndRun(classDefs: Seq[ClassDef], moduleInitializers: List[ModuleInitializer], linkerConfig: StandardConfig, inputKind: TestKit.InputKind): Future[Unit] = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala index 027258f65f..8fd9b501c8 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala @@ -31,6 +31,15 @@ object Infos { private val cloneMethodName = MethodName("clone", Nil, ClassRef(ObjectClass)) + /* Elements of WrapAsThrowable and UnwrapFromThrowable used by the Emitter + * In theory, these should be an implementation detail of the Emitter, and + * requested through `symbolRequirements`. However, doing so would mean that + * we would never be able to dead-code-eliminate JavaScriptException, which + * would be annoying. + */ + private val JavaScriptExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + private val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) + final case class NamespacedMethodName( namespace: MemberNamespace, methodName: MethodName) @@ -583,6 +592,13 @@ object Infos { case GetClass(_) => builder.addAccessedClassClass() + case WrapAsThrowable(_) => + builder.addUsedInstanceTest(ThrowableClass) + builder.addInstantiatedClass(JavaScriptExceptionClass, AnyArgConstructorName) + + case UnwrapFromThrowable(_) => + builder.addUsedInstanceTest(JavaScriptExceptionClass) + case JSPrivateSelect(qualifier, className, field) => builder.addPrivateJSFieldUsed(className, field.name) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala index 12bc927a79..d2c031f805 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala @@ -18,13 +18,19 @@ import org.scalajs.ir.Types._ private[emitter] object EmitterNames { // Class names + val JavaScriptExceptionClass = + ClassName("scala.scalajs.js.JavaScriptException") + val UndefinedBehaviorErrorClass = ClassName("org.scalajs.linker.runtime.UndefinedBehaviorError") - val ThrowableClass = ClassName("java.lang.Throwable") + // Field names + + val exceptionFieldName = FieldName("exception") // Method names + val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) val StringArgConstructorName = MethodName.constructor(List(ClassRef(BoxedStringClass))) val ThrowableArgConsructorName = MethodName.constructor(List(ClassRef(ThrowableClass))) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index c0725ac3ca..d7bfb80296 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -1270,6 +1270,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case IdentityHashCode(expr) => test(expr) case GetClass(arg) => test(arg) // may NPE but that is UB. + // Expressions preserving pureness but requiring that expr be a var + case WrapAsThrowable(expr @ (VarRef(_) | Transient(JSVarRef(_, _)))) => test(expr) + case UnwrapFromThrowable(expr @ (VarRef(_) | Transient(JSVarRef(_, _)))) => test(expr) + // Transients preserving pureness case Transient(ZeroOf(runtimeClass)) => test(runtimeClass) // may NPE but that is UB. @@ -1784,6 +1788,22 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { redo(IdentityHashCode(newExpr))(env) } + case WrapAsThrowable(expr) => + unnest(expr) { (newExpr, newEnv) => + implicit val env = newEnv + withTempJSVar(newExpr) { varRef => + redo(WrapAsThrowable(varRef)) + } + } + + case UnwrapFromThrowable(expr) => + unnest(expr) { (newExpr, newEnv) => + implicit val env = newEnv + withTempJSVar(newExpr) { varRef => + redo(UnwrapFromThrowable(varRef)) + } + } + case Transient(ZeroOf(runtimeClass)) => unnest(runtimeClass) { (newRuntimeClass, env) => redo(Transient(ZeroOf(newRuntimeClass)))(env) @@ -2002,6 +2022,20 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { }) } + private def withTempJSVar(value: Tree)(makeBody: Transient => js.Tree)( + implicit env: Env, pos: Position): js.Tree = { + withTempJSVar(transformExpr(value, value.tpe), value.tpe)(makeBody) + } + + private def withTempJSVar(value: js.Tree, tpe: Type)( + makeBody: Transient => js.Tree)( + implicit pos: Position): js.Tree = { + val varIdent = newSyntheticVar() + val varDef = genLet(varIdent, mutable = false, value) + val body = makeBody(Transient(JSVarRef(varIdent, mutable = false)(tpe))) + js.Block(varDef, body) + } + private def needsToTranslateAnySpread(args: List[TreeOrJSSpread]): Boolean = !es2015 && args.exists(_.isInstanceOf[JSSpread]) @@ -2629,6 +2663,20 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case IdentityHashCode(expr) => genCallHelper("systemIdentityHashCode", transformExprNoChar(expr)) + case WrapAsThrowable(expr) => + val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef] + js.If( + genIsInstanceOfClass(newExpr, ThrowableClass), + newExpr, + genScalaClassNew(JavaScriptExceptionClass, AnyArgConstructorName, newExpr)) + + case UnwrapFromThrowable(expr) => + val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef] + js.If( + genIsInstanceOfClass(newExpr, JavaScriptExceptionClass), + genSelect(newExpr, JavaScriptExceptionClass, FieldIdent(exceptionFieldName)), + newExpr) + // Transients case Transient(ZeroOf(runtimeClass)) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index 11c4a8b5b1..051906569b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -661,6 +661,12 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) case IdentityHashCode(expr) => checkTree(expr, env) + case WrapAsThrowable(expr) => + checkTree(expr, env) + + case UnwrapFromThrowable(expr) => + checkTree(expr, env) + // JavaScript expressions case JSNew(ctor, args) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 39890bc87f..4ecbbf159a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -550,6 +550,12 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { case IdentityHashCode(expr) => typecheckExpr(expr, env) + case WrapAsThrowable(expr) => + typecheckExpr(expr, env) + + case UnwrapFromThrowable(expr) => + typecheckExpect(expr, env, ClassType(ThrowableClass)) + // JavaScript expressions case JSNew(ctor, args) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 2c9605c148..fcad53fdbe 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -307,7 +307,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { implicit scope: Scope): Tree = { @inline implicit def pos = tree.pos - val result = tree match { + val result: Tree = tree match { // Definitions case VarDef(_, _, _, _, rhs) => @@ -591,6 +591,11 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case IdentityHashCode(expr) => IdentityHashCode(transformExpr(expr)) + case _:WrapAsThrowable | _:UnwrapFromThrowable => + trampoline { + pretransformExpr(tree)(finishTransform(isStat)) + } + // JavaScript expressions case JSNew(ctor, args) => @@ -944,6 +949,48 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case tree: BinaryOp => pretransformBinaryOp(tree)(cont) + case WrapAsThrowable(expr) => + pretransformExpr(expr) { texpr => + def default = { + val refinedType: RefinedType = RefinedType(ThrowableClassType, isExact = false, isNullable = false) + cont(PreTransTree(WrapAsThrowable(finishTransformExpr(texpr)), refinedType)) + } + + if (isSubtype(texpr.tpe.base, ThrowableClassType)) { + if (texpr.tpe.isNullable) + default + else + cont(texpr) + } else { + if (texpr.tpe.isExact) { + pretransformNew(AllocationSite.Tree(tree), JavaScriptExceptionClass, + MethodIdent(AnyArgConstructorName), texpr :: Nil)(cont) + } else { + default + } + } + } + + case UnwrapFromThrowable(expr) => + pretransformExpr(expr) { texpr => + def default = + cont(PreTransTree(UnwrapFromThrowable(finishTransformExpr(texpr)))) + + if (isSubtype(texpr.tpe.base, JavaScriptExceptionClassType)) { + if (texpr.tpe.isNullable) { + default + } else { + pretransformSelectCommon(AnyType, texpr, JavaScriptExceptionClass, + FieldIdent(exceptionFieldName), isLhsOfAssign = false)(cont) + } + } else { + if (texpr.tpe.isExact || !isSubtype(JavaScriptExceptionClassType, texpr.tpe.base)) + cont(texpr) + else + default + } + } + case tree: JSSelect => pretransformJSSelect(tree, isLhsOfAssign = false)(cont) @@ -1543,6 +1590,10 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { Block(elems.map(keepOnlySideEffects))(stat.pos) case RecordSelect(record, _) => keepOnlySideEffects(record) + case WrapAsThrowable(expr) => + keepOnlySideEffects(expr) + case UnwrapFromThrowable(expr) => + keepOnlySideEffects(expr) case _ => stat } @@ -4699,10 +4750,18 @@ private[optimizer] object OptimizerCore { private val thisOriginalName: OriginalName = OriginalName("this") - private val Tuple2Class = ClassName("scala.Tuple2") - private val NilClass = ClassName("scala.collection.immutable.Nil$") - private val JSWrappedArrayClass = ClassName("scala.scalajs.js.WrappedArray") private val ClassTagModuleClass = ClassName("scala.reflect.ClassTag$") + private val JavaScriptExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + private val JSWrappedArrayClass = ClassName("scala.scalajs.js.WrappedArray") + private val NilClass = ClassName("scala.collection.immutable.Nil$") + private val Tuple2Class = ClassName("scala.Tuple2") + + private val JavaScriptExceptionClassType = ClassType(JavaScriptExceptionClass) + private val ThrowableClassType = ClassType(ThrowableClass) + + private val exceptionFieldName = FieldName("exception") + + private val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) private val ObjectCloneName = MethodName("clone", Nil, ClassRef(ObjectClass)) private val TupleFirstMethodName = MethodName("_1", Nil, ClassRef(ObjectClass)) diff --git a/project/Build.scala b/project/Build.scala index ebf4aca8c5..dc9c3ff673 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1741,15 +1741,15 @@ object Build { case Default2_12ScalaVersion => Some(ExpectedSizes( - fastLink = 779000 to 780000, + fastLink = 777000 to 778000, fullLink = 148000 to 149000, - fastLinkGz = 91000 to 92000, + fastLinkGz = 90000 to 91000, fullLinkGz = 36000 to 37000, )) case Default2_13ScalaVersion => Some(ExpectedSizes( - fastLink = 729000 to 730000, + fastLink = 728000 to 729000, fullLink = 156000 to 157000, fastLinkGz = 91000 to 92000, fullLinkGz = 40000 to 41000, diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 70d9a046fd..8b784e8f0e 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -488,7 +488,10 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } private def validateClassName(cls: ClassName)(implicit pos: Position): Unit = { - if (cls.nameString.startsWith("scala.")) + def isJavaScriptExceptionWithinItself = + cls == JavaScriptExceptionClass && enclosingClassName == JavaScriptExceptionClass + + if (cls.nameString.startsWith("scala.") && !isJavaScriptExceptionWithinItself) reportError(s"Illegal reference to Scala class ${cls.nameString}") } @@ -515,6 +518,9 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { object JavalibIRCleaner { private final val MaxFunctionArity = 4 + // Within js.JavaScriptException, which is part of the linker private lib, we can refer to itself + private val JavaScriptExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + private val JavaIOSerializable = ClassName("java.io.Serializable") private val JSAny = ClassName("scala.scalajs.js.Any") private val JSAnyMod = ClassName("scala.scalajs.js.Any$") diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala index 9f2347610d..365b6bc66e 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala @@ -157,6 +157,26 @@ class OptimizerTest { // scalastyle:on return } + @Test def preserveSideEffectsInUnwrapFromThrowable(): Unit = { + /* This is also indirectly serves as a test for WrapAsThrowable. It's not + * possible to write user-level Scala code that produces a WrapAsThrowable + * with anything but a VarRef. But since WrapAsThrowable is handled exactly + * like UnwrapFromThrowable in the linker, this test somewhat covers both. + */ + + var i: Int = 1 + try { + if (i > 0) + throw ({ i += 1; new js.JavaScriptException(i) }) + i = -1 // unreachable + } catch { + case js.JavaScriptException(x) => + assertEquals(2, x) + assertEquals(2, i) + } + assertEquals(2, i) + } + // === constant folding @Test def constantFoldingEqEqEq(): Unit = { diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/JavaScriptExceptionTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/JavaScriptExceptionTest.scala index 9415819c5b..4e8ff1390a 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/JavaScriptExceptionTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/JavaScriptExceptionTest.scala @@ -33,4 +33,21 @@ class JavaScriptExceptionTest { jsException.toString()) } + @Test def caseClassAPI(): Unit = { + /* This is mostly to ensure that the js.JavaScriptException from the + * scalajs-library takes precedence over the one from the + * linker-private-library. + */ + + val error = new js.TypeError("custom message") + val jsException = js.JavaScriptException(error) + val jsException2 = jsException.copy() + assertNotSame(jsException, jsException2) + assertSame(error, jsException2.exception) + + val product: Product = jsException + assertEquals("JavaScriptException", product.productPrefix) + assertSame(error, product.productElement(0)) + } + } From 994a59892f1c6d17e9bdb3f3bf81cd49a099660d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 1 Jul 2022 14:46:02 +0200 Subject: [PATCH 11/75] Add `js.special` primitives for JS exception handling. * `js.special.throw(e)` throws any value `e`. * `js.special.tryCatch(() => body)(e => handler)` catches any exception thrown by the body and handles it in the handler. * `js.special.wrapAsThrowable(e)` wraps `e` in a `JavaScriptException` if it is not a `Throwable`. * `js.special.unwrapFromThrowable(th)` unwraps `JavaScriptException`s and returns other values as is. These primitives can be used to implement some fundamental operations in a more direct way. --- .../org/scalajs/nscplugin/GenJSCode.scala | 47 +++++++ .../org/scalajs/nscplugin/JSDefinitions.scala | 4 + .../org/scalajs/nscplugin/JSPrimitives.scala | 20 ++- .../scala/scalajs/js/JSConverters.scala | 5 +- .../scala/scalajs/js/JSConverters.scala | 5 +- .../scala/scala/scalajs/js/Thenable.scala | 5 +- .../scala/scalajs/js/special/package.scala | 44 +++++++ .../scala/scala/scalajs/runtime/package.scala | 16 +-- .../testsuite/compiler/OptimizerTest.scala | 26 +++- .../testsuite/jsinterop/SpecialTest.scala | 124 ++++++++++++++++++ .../jsinterop/ThrowAndCatchTest.scala | 80 +++++++++-- 11 files changed, 330 insertions(+), 46 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 3af61fbb70..3ad1ccabec 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -5288,6 +5288,53 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.ForIn(objVarDef.ref, keyVarIdent, NoOriginalName, { js.JSFunctionApply(fVarDef.ref, List(keyVarRef)) })) + + case JS_THROW => + // js.special.throw(arg) + js.Throw(genArgs1) + + case JS_TRY_CATCH => + /* js.special.tryCatch(arg1, arg2) + * + * We must generate: + * + * val body = arg1 + * val handler = arg2 + * try { + * body() + * } catch (e) { + * handler(e) + * } + * + * with temporary vals, because `arg2` must be evaluated before + * `body` executes. Moreover, exceptions thrown while evaluating + * the function values `arg1` and `arg2` must not be caught. + */ + val (arg1, arg2) = genArgs2 + val bodyVarDef = js.VarDef(freshLocalIdent("body"), NoOriginalName, + jstpe.AnyType, mutable = false, arg1) + val handlerVarDef = js.VarDef(freshLocalIdent("handler"), NoOriginalName, + jstpe.AnyType, mutable = false, arg2) + val exceptionVarIdent = freshLocalIdent("e") + val exceptionVarRef = js.VarRef(exceptionVarIdent)(jstpe.AnyType) + js.Block( + bodyVarDef, + handlerVarDef, + js.TryCatch( + js.JSFunctionApply(bodyVarDef.ref, Nil), + exceptionVarIdent, + NoOriginalName, + js.JSFunctionApply(handlerVarDef.ref, List(exceptionVarRef)) + )(jstpe.AnyType) + ) + + case WRAP_AS_THROWABLE => + // js.special.wrapAsThrowable(arg) + js.WrapAsThrowable(genArgs1) + + case UNWRAP_FROM_THROWABLE => + // js.special.unwrapFromThrowable(arg) + js.UnwrapFromThrowable(genArgs1) } } diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala index 1724270ea4..d52b86af79 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala @@ -107,6 +107,10 @@ trait JSDefinitions { lazy val Special_instanceof = getMemberMethod(SpecialPackageModule, newTermName("instanceof")) lazy val Special_delete = getMemberMethod(SpecialPackageModule, newTermName("delete")) lazy val Special_forin = getMemberMethod(SpecialPackageModule, newTermName("forin")) + lazy val Special_throw = getMemberMethod(SpecialPackageModule, newTermName("throw")) + lazy val Special_tryCatch = getMemberMethod(SpecialPackageModule, newTermName("tryCatch")) + lazy val Special_wrapAsThrowable = getMemberMethod(SpecialPackageModule, newTermName("wrapAsThrowable")) + lazy val Special_unwrapFromThrowable = getMemberMethod(SpecialPackageModule, newTermName("unwrapFromThrowable")) lazy val Special_debugger = getMemberMethod(SpecialPackageModule, newTermName("debugger")) lazy val RuntimePackageModule = getPackageObject("scala.scalajs.runtime") diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala index ade5ab2c2c..df5ff293db 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala @@ -59,12 +59,16 @@ abstract class JSPrimitives { final val IDENTITY_HASH_CODE = LINKING_INFO + 1 // runtime.identityHashCode final val DYNAMIC_IMPORT = IDENTITY_HASH_CODE + 1 // runtime.dynamicImport - final val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals - final val IN = STRICT_EQ + 1 // js.special.in - final val INSTANCEOF = IN + 1 // js.special.instanceof - final val DELETE = INSTANCEOF + 1 // js.special.delete - final val FORIN = DELETE + 1 // js.special.forin - final val DEBUGGER = FORIN + 1 // js.special.debugger + final val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals + final val IN = STRICT_EQ + 1 // js.special.in + final val INSTANCEOF = IN + 1 // js.special.instanceof + final val DELETE = INSTANCEOF + 1 // js.special.delete + final val FORIN = DELETE + 1 // js.special.forin + final val JS_THROW = FORIN + 1 // js.special.throw + final val JS_TRY_CATCH = JS_THROW + 1 // js.special.tryCatch + 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 LastJSPrimitiveCode = DEBUGGER @@ -114,6 +118,10 @@ abstract class JSPrimitives { addPrimitive(Special_instanceof, INSTANCEOF) addPrimitive(Special_delete, DELETE) addPrimitive(Special_forin, FORIN) + addPrimitive(Special_throw, JS_THROW) + addPrimitive(Special_tryCatch, JS_TRY_CATCH) + addPrimitive(Special_wrapAsThrowable, WRAP_AS_THROWABLE) + addPrimitive(Special_unwrapFromThrowable, UNWRAP_FROM_THROWABLE) addPrimitive(Special_debugger, DEBUGGER) } diff --git a/library/src/main/scala-new-collections/scala/scalajs/js/JSConverters.scala b/library/src/main/scala-new-collections/scala/scalajs/js/JSConverters.scala index b82b1daa41..6c89652484 100644 --- a/library/src/main/scala-new-collections/scala/scalajs/js/JSConverters.scala +++ b/library/src/main/scala-new-collections/scala/scalajs/js/JSConverters.scala @@ -172,10 +172,7 @@ object JSConverters extends JSConvertersLowPrioImplicits { resolve(value) case scala.util.Failure(th) => - reject(th match { - case js.JavaScriptException(e) => e - case _ => th - }) + reject(js.special.unwrapFromThrowable(th)) } }) } diff --git a/library/src/main/scala-old-collections/scala/scalajs/js/JSConverters.scala b/library/src/main/scala-old-collections/scala/scalajs/js/JSConverters.scala index 753f54c615..dd3828ad6f 100644 --- a/library/src/main/scala-old-collections/scala/scalajs/js/JSConverters.scala +++ b/library/src/main/scala-old-collections/scala/scalajs/js/JSConverters.scala @@ -182,10 +182,7 @@ object JSConverters extends js.JSConvertersLowPrioImplicits { resolve(value) case scala.util.Failure(th) => - reject(th match { - case js.JavaScriptException(e) => e - case _ => th - }) + reject(js.special.unwrapFromThrowable(th)) } }) } diff --git a/library/src/main/scala/scala/scalajs/js/Thenable.scala b/library/src/main/scala/scala/scalajs/js/Thenable.scala index ca7c36b91e..dcf16b95c5 100644 --- a/library/src/main/scala/scala/scalajs/js/Thenable.scala +++ b/library/src/main/scala/scala/scalajs/js/Thenable.scala @@ -59,10 +59,7 @@ object Thenable { (): Unit | js.Thenable[Unit] }, js.defined { (e: scala.Any) => - p2.failure(e match { - case th: Throwable => th - case _ => js.JavaScriptException(e) - }) + p2.failure(js.special.wrapAsThrowable(e)) (): Unit | js.Thenable[Unit] }) p2.future diff --git a/library/src/main/scala/scala/scalajs/js/special/package.scala b/library/src/main/scala/scala/scalajs/js/special/package.scala index dceb55733e..1c2437bff9 100644 --- a/library/src/main/scala/scala/scalajs/js/special/package.scala +++ b/library/src/main/scala/scala/scalajs/js/special/package.scala @@ -135,6 +135,50 @@ package object special { def forin(obj: scala.Any)(f: js.Function1[scala.Any, scala.Any]): Unit = throw new java.lang.Error("stub") + /** Throw an arbitrary value, which will be caught as is by a JavaScript + * `try..catch` statement. + * + * Usually, a Scala `throw` expression is more appropriate. Even if you + * want to throw a JS error type such as [[js.Error]], it is more idiomatic + * to wrap it in a [[js.JavaScriptException]] and throw that one. + * + * However, if you hold a value of an arbitrary type, which was caught by a + * JavaScript `try..catch` statement (sometimes indirectly, such as with + * [[js.Promise]]s), it is appropriate to use `js.special.throw` to rethrow + * it. + */ + def `throw`(ex: scala.Any): Nothing = + throw new java.lang.Error("stub") + + /** Performs a JavaScript `try..catch`, which can catch any type of value. + * + * Usually, a Scala `try..catch` expression catching [[Throwable]] is more + * appropriate. Values that are not instances of [[Throwable]], such as JS + * error values, are then wrapped in a [[js.JavaScriptException]]. + * + * However, if you need to get the originally thrown value, for example to + * pass it on to a JavaScript error handler, it is appropriate to use + * `js.special.tryCatch`. + */ + def tryCatch[A](body: js.Function0[A])(handler: js.Function1[scala.Any, A]): A = + throw new java.lang.Error("stub") + + /** Wrap any value so that it can be assigned to a [[Throwable]]. + * + * Instances of [[Throwable]] are returned as is. Other values are wrapped + * in a [[js.JavaScriptException]]. + */ + def wrapAsThrowable(ex: scala.Any): Throwable = + throw new java.lang.Error("stub") + + /** Unwrap an exception value wrapped with `wrapAsThrowable`. + * + * Instances of [[js.JavaScriptException]] are unwrapped to return the + * underlying value. Other values are returned as is. + */ + def unwrapFromThrowable(th: Throwable): scala.Any = + throw new java.lang.Error("stub") + /** The value of the JavaScript `this` at the top-level of the generated * file. * diff --git a/library/src/main/scala/scala/scalajs/runtime/package.scala b/library/src/main/scala/scala/scalajs/runtime/package.scala index 7aff7c9696..151769c2b9 100644 --- a/library/src/main/scala/scala/scalajs/runtime/package.scala +++ b/library/src/main/scala/scala/scalajs/runtime/package.scala @@ -18,17 +18,13 @@ package object runtime { import scala.scalajs.runtime.Compat._ - @deprecated("Unused by the codegen", "1.11.0") - def wrapJavaScriptException(e: Any): Throwable = e match { - case e: Throwable => e - case _ => js.JavaScriptException(e) - } + @deprecated("Unused by the codegen; use js.special.wrapAsThrowable instead", "1.11.0") + @inline def wrapJavaScriptException(e: Any): Throwable = + js.special.wrapAsThrowable(e) - @deprecated("Unused by the codegen", "1.11.0") - def unwrapJavaScriptException(th: Throwable): Any = th match { - case js.JavaScriptException(e) => e - case _ => th - } + @deprecated("Unused by the codegen; use js.special.unwrapFromThrowable instead", "1.11.0") + @inline def unwrapJavaScriptException(th: Throwable): Any = + js.special.unwrapFromThrowable(th) @inline def toScalaVarArgs[A](array: js.Array[A]): Seq[A] = toScalaVarArgsImpl(array) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala index 365b6bc66e..f7ea669d8f 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala @@ -157,13 +157,29 @@ class OptimizerTest { // scalastyle:on return } + @Test def preserveSideEffectsInWrapAsThrowable(): Unit = { + var i: Int = 1 + val x = + if (i > 0) js.special.wrapAsThrowable({ i += 1; i }) + else 42 + + x match { + case js.JavaScriptException(y) => + assertEquals(2, y) + } + assertEquals(2, i) + } + @Test def preserveSideEffectsInUnwrapFromThrowable(): Unit = { - /* This is also indirectly serves as a test for WrapAsThrowable. It's not - * possible to write user-level Scala code that produces a WrapAsThrowable - * with anything but a VarRef. But since WrapAsThrowable is handled exactly - * like UnwrapFromThrowable in the linker, this test somewhat covers both. - */ + var i: Int = 1 + val x = + if (i > 0) js.special.unwrapFromThrowable({ i += 1; new js.JavaScriptException(i) }) + else 42 + assertEquals(2, x) + assertEquals(2, i) + } + @Test def preserveSideEffectsInUnwrapFromThrowableInThrow(): Unit = { var i: Int = 1 try { if (i > 0) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala index cd4d74696d..af26ab603b 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala @@ -105,6 +105,130 @@ class SpecialTest { js.special.delete(a[js.Object]("foo"), kh.key) } + // js.special.tryCatch + + @Test def jsThrow(): Unit = { + val e = assertThrows(classOf[js.JavaScriptException], js.special.`throw`("foo")) + assertEquals("foo", e.exception) + + assertThrows(classOf[IllegalArgumentException], js.special.`throw`(new IllegalArgumentException)) + } + + @Test def jsTryCatch(): Unit = { + @noinline def interrupt(): Unit = throw new IllegalStateException + + // No exception + locally { + var order = "0" + js.special.tryCatch({ + order += "1" + + { () => order += "3" } + })({ + order += "2" + + { (e: Any) => fail("no exception should be thrown and caught") } + }) + assertEquals("0123", order) + } + + // Exception thrown during execution of the body + locally { + var order = "0" + js.special.tryCatch({ + order += "1" + + { () => + order += "3" + interrupt() + } + })({ + order += "2" + + { (e: Any) => + order += "4" + assertTrue(e.isInstanceOf[IllegalStateException]) + } + }) + assertEquals("01234", order) + } + + // Exception thrown when computing the body + locally { + var order = "0" + assertThrows(classOf[IllegalStateException], { + js.special.tryCatch({ + order += "1" + interrupt() + + { () => fail("unreachable 1") } + })({ + fail("unreachable 2") + + { (e: Any) => fail("unreachable 3") } + }) + }) + assertEquals("01", order) + } + + // Exception thrown when computing the handler + locally { + var order = "0" + assertThrows(classOf[IllegalStateException], { + js.special.tryCatch({ + order += "1" + + { () => fail("unreachable 1") } + })({ + order += "2" + interrupt() + + { (e: Any) => fail("unreachable 2") } + }) + }) + assertEquals("012", order) + } + } + + // js.special.wrapAsThrowable + + @Test def wrapAsThrowable(): Unit = { + // Wraps a js.Object + val obj = new js.Object + val e1 = js.special.wrapAsThrowable(obj) + e1 match { + case js.JavaScriptException(o) => assertSame(obj, o) + } + + // Wraps null + val e2 = js.special.wrapAsThrowable(null) + e2 match { + case js.JavaScriptException(v) => assertNull(v) + } + + // Does not wrap a Throwable + val th = new IllegalArgumentException + assertSame(th, js.special.wrapAsThrowable(th)) + + // Does not double-wrap + assertSame(e1, js.special.wrapAsThrowable(e1)) + } + + // js.special.unwrapFromThrowable + + @Test def unwrapFromThrowable(): Unit = { + // Unwraps a JavaScriptException + val obj = new js.Object + assertSame(obj, js.special.unwrapFromThrowable(js.JavaScriptException(obj))) + + // Does not unwrap a Throwable + val th = new IllegalArgumentException + assertSame(th, js.special.unwrapFromThrowable(th)) + + // Does not unwrap null + assertNull(null, js.special.unwrapFromThrowable(null)) + } + // js.special.fileLevelThis @Test def fileLevelThisCanBeUsedToDetectTheGlobalObject(): Unit = { diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ThrowAndCatchTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ThrowAndCatchTest.scala index 086725d843..9f202e40c2 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ThrowAndCatchTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ThrowAndCatchTest.scala @@ -40,6 +40,16 @@ class ThrowAndCatchTest { } } + @Test def testJSThrowTypeErrorScalaCatchFuture(): Unit = { + val e = new js.TypeError("boom") + try { + jsThrow(e) + } catch { + case JSExceptionFuture(e2) => + assertSame(e, e2) + } + } + @Test def testJSThrowOptionScalaCatch(): Unit = { val e = Some("boom") try { @@ -50,6 +60,16 @@ class ThrowAndCatchTest { } } + @Test def testJSThrowOptionScalaCatchFuture(): Unit = { + val e = Some("boom") + try { + jsThrow(e) + } catch { + case JSExceptionFuture(e2) => + assertSame(e, e2) + } + } + @Test def testScalaThrowThrowableJSCatch(): Unit = { val e = new Exception("boom") val e2 = jsCatch(() => throw e) @@ -62,12 +82,24 @@ class ThrowAndCatchTest { assertSame(e, e2) } + @Test def testScalaThrowTypeErrorJSCatchFuture(): Unit = { + val e = new js.TypeError("boom") + val e2 = jsCatch(() => throw JSExceptionFuture(e)) + assertSame(e, e2) + } + @Test def testScalaThrowOptionJSCatch(): Unit = { val e = Some("boom") val e2 = jsCatch(() => throw js.JavaScriptException(e)) assertSame(e, e2) } + @Test def testScalaThrowOptionJSCatchFuture(): Unit = { + val e = Some("boom") + val e2 = jsCatch(() => throw JSExceptionFuture(e)) + assertSame(e, e2) + } + @Test def testScalaThrowThrowableInJavaScriptExceptionJSCatch(): Unit = { // This is evil, but spec'ed nevertheless val e = new Exception("boom") @@ -75,21 +107,43 @@ class ThrowAndCatchTest { assertSame(e, e2) } + @Test def testScalaThrowThrowableInJavaScriptExceptionJSCatchFuture(): Unit = { + // This is evil, but spec'ed nevertheless + val e = new Exception("boom") + val e2 = jsCatch(() => throw JSExceptionFuture(e)) + assertSame(e, e2) + } + } object ThrowAndCatchTest { - private val jsThrow: js.Function1[Any, Nothing] = - new js.Function("e", "throw e;").asInstanceOf[js.Function1[Any, Nothing]] - - private val jsCatch: js.Function1[js.Function0[_], Any] = { - new js.Function("f", - """ - |try { - | f(); - |} catch (e) { - | return e; - |} - |throw new Error("Did not catch anything"); - """.stripMargin).asInstanceOf[js.Function1[js.Function0[_], Any]] + @noinline + def jsThrow(ex: Any): Nothing = + js.special.`throw`(ex) + + @noinline + def jsCatch(body: js.Function0[Any]): Any = { + val thrown = js.special.tryCatch[Option[Any]] { () => + body() + None + } { (ex: Any) => + Some(ex) + } + thrown.getOrElse { + throw new AssertionError("Did not catch anything") + } + } + + /** `js.JavaScriptException` as we would define it in Scala.js 2.x. */ + object JSExceptionFuture { + @inline def apply(ex: Any): Throwable = js.special.wrapAsThrowable(ex) + + @inline def unapply(th: Throwable): Option[Any] = { + val ex = js.special.unwrapFromThrowable(th) + if (th eq ex.asInstanceOf[AnyRef]) + None + else + Some(ex) + } } } From 9e02422f72f0ddf65dc47980b49e820e66488650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 11 Jul 2022 10:10:39 +0200 Subject: [PATCH 12/75] Repair the import for bloop-based IDEs. This was subtly broken in 478c7a48099bb337ddc0793fae05248f9bed2bcb because the projects' settings want to `+=` to `buildInfoOptions`. When we don't include the settings of `BuildInfoPlugin`, there is no initial value for that setting, and therefore the build fails. --- project/Build.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index dc9c3ff673..d31c011828 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -287,8 +287,9 @@ object Build { private def buildInfoOrStubs(config: Configuration, stubsBaseDir: Def.Initialize[File]) = { if (isGeneratingForIDE) { Def.settings( - unmanagedSourceDirectories in config += - stubsBaseDir.value / "scala-ide-stubs" + unmanagedSourceDirectories in config += + stubsBaseDir.value / "scala-ide-stubs", + config / buildInfoOptions := Nil, ) } else { Def.settings( From 772346f6bfb95611b38ab437b67d8852f4c0da7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 8 Dec 2021 16:56:56 +0100 Subject: [PATCH 13/75] IR cleaner: Rewrite s.r.XRef types to j.u.internal.XRef. This way, we can keep using `var`s that are captured in the javalib, without keeping references to `scala.runtime.XRef` types. --- .../scala/java/util/internal/RefTypes.scala | 94 +++++++++++++++++++ project/JavalibIRCleaner.scala | 12 ++- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 javalib/src/main/scala/java/util/internal/RefTypes.scala diff --git a/javalib/src/main/scala/java/util/internal/RefTypes.scala b/javalib/src/main/scala/java/util/internal/RefTypes.scala new file mode 100644 index 0000000000..d02cf33d8d --- /dev/null +++ b/javalib/src/main/scala/java/util/internal/RefTypes.scala @@ -0,0 +1,94 @@ +/* + * 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.internal + +@inline +private[java] class BooleanRef(var elem: Boolean) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object BooleanRef { + def create(elem: Boolean): BooleanRef = new BooleanRef(elem) + def zero(): BooleanRef = new BooleanRef(false) +} + +@inline +private[java] class CharRef(var elem: Char) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object CharRef { + def create(elem: Char): CharRef = new CharRef(elem) + def zero(): CharRef = new CharRef(0.toChar) +} + +@inline +private[java] class ByteRef(var elem: Byte) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object ByteRef { + def create(elem: Byte): ByteRef = new ByteRef(elem) + def zero(): ByteRef = new ByteRef(0) +} + +@inline +private[java] class ShortRef(var elem: Short) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object ShortRef { + def create(elem: Short): ShortRef = new ShortRef(elem) + def zero(): ShortRef = new ShortRef(0) +} + +@inline +private[java] class IntRef(var elem: Int) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object IntRef { + def create(elem: Int): IntRef = new IntRef(elem) + def zero(): IntRef = new IntRef(0) +} + +@inline +private[java] class LongRef(var elem: Long) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object LongRef { + def create(elem: Long): LongRef = new LongRef(elem) + def zero(): LongRef = new LongRef(0) +} + +@inline +private[java] class FloatRef(var elem: Float) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object FloatRef { + def create(elem: Float): FloatRef = new FloatRef(elem) + def zero(): FloatRef = new FloatRef(0) +} + +@inline +private[java] class DoubleRef(var elem: Double) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object DoubleRef { + def create(elem: Double): DoubleRef = new DoubleRef(elem) + def zero(): DoubleRef = new DoubleRef(0) +} + +@inline +private[java] class ObjectRef[A](var elem: A) extends Serializable { + override def toString(): String = String.valueOf(elem) +} +private[java] object ObjectRef { + def create[A](elem: A): ObjectRef[A] = new ObjectRef(elem) + def zero(): ObjectRef[Object] = new ObjectRef(null) +} diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 8b784e8f0e..eeda04598c 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -603,6 +603,16 @@ object JavalibIRCleaner { funClass -> ObjectClass } - functionTypePairs.toMap + val refBaseNames = + List("Boolean", "Char", "Byte", "Short", "Int", "Long", "Float", "Double", "Object") + val refPairs = for { + refBaseName <- refBaseNames + } yield { + val simpleName = refBaseName + "Ref" + ClassName("scala.runtime." + simpleName) -> ClassName("java.util.internal." + simpleName) + } + + val allPairs = functionTypePairs ++ refPairs + allPairs.toMap } } From 2c613da08c8d5c7b60714c24d051d1dfc89dc474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 30 May 2022 17:22:51 +0200 Subject: [PATCH 14/75] IR cleaner: Rewrite references to Tuples to j.u.internal.Tuples. --- .../scala/java/util/internal/Tuples.scala | 26 +++++++++++++++++++ project/Build.scala | 2 +- project/JavalibIRCleaner.scala | 8 +++++- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 javalib/src/main/scala/java/util/internal/Tuples.scala diff --git a/javalib/src/main/scala/java/util/internal/Tuples.scala b/javalib/src/main/scala/java/util/internal/Tuples.scala new file mode 100644 index 0000000000..d476cd74a9 --- /dev/null +++ b/javalib/src/main/scala/java/util/internal/Tuples.scala @@ -0,0 +1,26 @@ +/* + * 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.internal + +@inline +final class Tuple2[+T1, +T2](val _1: T1, val _2: T2) + +@inline +final class Tuple3[+T1, +T2, +T3](val _1: T1, val _2: T2, val _3: T3) + +@inline +final class Tuple4[+T1, +T2, +T3, +T4](val _1: T1, val _2: T2, val _3: T3, val _4: T4) + +@inline +final class Tuple8[+T1, +T2, +T3, +T4, +T5, +T6, +T7, +T8]( + val _1: T1, val _2: T2, val _3: T3, val _4: T4, val _5: T5, val _6: T6, val _7: T7, val _8: T8) diff --git a/project/Build.scala b/project/Build.scala index d31c011828..552563ebde 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -572,7 +572,7 @@ object Build { ) val cleanIRSettings = Def.settings( - // In order to rewrite anonymous functions, the code must not be specialized + // In order to rewrite anonymous functions and tuples, the code must not be specialized scalacOptions += "-no-specialization", products in Compile := { diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index eeda04598c..e0d3010e85 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -612,7 +612,13 @@ object JavalibIRCleaner { ClassName("scala.runtime." + simpleName) -> ClassName("java.util.internal." + simpleName) } - val allPairs = functionTypePairs ++ refPairs + val tuplePairs = for { + n <- (2 to 22).toList + } yield { + ClassName("scala.Tuple" + n) -> ClassName("java.util.internal.Tuple" + n) + } + + val allPairs = functionTypePairs ++ refPairs ++ tuplePairs allPairs.toMap } } From 2751d3576e3b311ece9e90199b67c8713bd07a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 31 May 2022 15:45:03 +0200 Subject: [PATCH 15/75] IR cleaner: Rewrite JS array vararg expansion. --- project/JavalibIRCleaner.scala | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index e0d3010e85..d4cf645df9 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -244,10 +244,27 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { if isFunctionNType(n, fun.tpe) => JSFunctionApply(fun, args) + // <= 2.12 : toJSVarArgs(jsArrayOps(jsArray).toSeq) -> jsArray + case IntrinsicCall(ScalaJSRuntimeMod, `toJSVarArgsReadOnlyMethodName`, + List(Apply( + ApplyFlags.empty, + IntrinsicCall(JSAnyMod, `jsArrayOpsToArrayOpsMethodName`, List(jsArray)), + MethodIdent(`toReadOnlySeqMethodName`), + Nil) + )) => + jsArray + + // >= 2.13 : toJSVarArgs(toSeq$extension(jsArray)) -> jsArray + case IntrinsicCall(ScalaJSRuntimeMod, `toJSVarArgsImmutableMethodName`, + List(IntrinsicCall(JSArrayOpsMod, `toImmutableSeqExtensionMethodName`, List(jsArray)))) => + jsArray + case IntrinsicCall(JSAnyMod, `jsAnyFromIntMethodName`, List(arg)) => arg case IntrinsicCall(JSAnyMod, `jsAnyFromStringMethodName`, List(arg)) => arg + case IntrinsicCall(JSAnyMod, `jsArrayOpsToArrayMethodName`, List(arg)) => + arg case IntrinsicCall(JSDynamicImplicitsMod, `number2dynamicMethodName`, List(arg)) => arg case IntrinsicCall(JSNumberOpsMod, `enableJSNumberOpsDoubleMethodName`, List(arg)) => @@ -521,10 +538,13 @@ object JavalibIRCleaner { // Within js.JavaScriptException, which is part of the linker private lib, we can refer to itself private val JavaScriptExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + private val ImmutableSeq = ClassName("scala.collection.immutable.Seq") private val JavaIOSerializable = ClassName("java.io.Serializable") private val JSAny = ClassName("scala.scalajs.js.Any") private val JSAnyMod = ClassName("scala.scalajs.js.Any$") private val JSArray = ClassName("scala.scalajs.js.Array") + private val JSArrayOps = ClassName("scala.scalajs.js.ArrayOps") + private val JSArrayOpsMod = ClassName("scala.scalajs.js.ArrayOps$") private val JSDynamic = ClassName("scala.scalajs.js.Dynamic") private val JSDynamicImplicitsMod = ClassName("scala.scalajs.js.DynamicImplicits$") private val JSNumberOps = ClassName("scala.scalajs.js.JSNumberOps") @@ -552,14 +572,26 @@ object JavalibIRCleaner { MethodName("fromInt", List(IntRef), ClassRef(JSAny)) private val jsAnyFromStringMethodName = MethodName("fromString", List(ClassRef(BoxedStringClass)), ClassRef(JSAny)) + private val jsArrayOpsToArrayMethodName = + MethodName("jsArrayOps", List(ClassRef(JSArray)), ClassRef(JSArray)) + private val jsArrayOpsToArrayOpsMethodName = + MethodName("jsArrayOps", List(ClassRef(JSArray)), ClassRef(JSArrayOps)) private val number2dynamicMethodName = MethodName("number2dynamic", List(DoubleRef), ClassRef(JSDynamic)) private val sMethodName = MethodName("s", List(ClassRef(ReadOnlySeq)), ClassRef(BoxedStringClass)) private val stringContextCtorMethodName = MethodName.constructor(List(ClassRef(ReadOnlySeq))) + private val toImmutableSeqExtensionMethodName = + MethodName("toSeq$extension", List(ClassRef(JSArray)), ClassRef(ImmutableSeq)) + private val toJSVarArgsImmutableMethodName = + MethodName("toJSVarArgs", List(ClassRef(ImmutableSeq)), ClassRef(JSArray)) + private val toJSVarArgsReadOnlyMethodName = + MethodName("toJSVarArgs", List(ClassRef(ReadOnlySeq)), ClassRef(JSArray)) private val toScalaVarArgsReadOnlyMethodName = MethodName("toScalaVarArgs", List(ClassRef(JSArray)), ClassRef(ReadOnlySeq)) + private val toReadOnlySeqMethodName = + MethodName("toSeq", Nil, ClassRef(ReadOnlySeq)) private val truthValueMethodName = MethodName("truthValue", List(ClassRef(JSDynamic)), BooleanRef) private val writeReplaceMethodName = From dac34eecce98aebfef9b220570e04f8d39e60ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 31 May 2022 15:58:05 +0200 Subject: [PATCH 16/75] IR cleaner: Erase calls to `|.from(arg, evidence)`. --- project/JavalibIRCleaner.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index d4cf645df9..4d5f22d5eb 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -273,6 +273,8 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { arg case IntrinsicCall(JSStringOpsMod, `enableJSStringOpsMethodName`, List(arg)) => arg + case IntrinsicCall(UnionTypeMod, `unionTypeFromMethodName`, List(arg, _)) => + arg case IntrinsicCall(JSDynamicImplicitsMod, `truthValueMethodName`, List(arg)) => AsInstanceOf( @@ -555,6 +557,9 @@ object JavalibIRCleaner { private val ScalaSerializable = ClassName("scala.Serializable") private val ScalaJSRuntimeMod = ClassName("scala.scalajs.runtime.package$") private val StringContextClass = ClassName("scala.StringContext") + private val UnionType = ClassName("scala.scalajs.js.$bar") + private val UnionTypeMod = ClassName("scala.scalajs.js.$bar$") + private val UnionTypeEvidence = ClassName("scala.scalajs.js.$bar$Evidence") private val FunctionNClasses: IndexedSeq[ClassName] = (0 to MaxFunctionArity).map(n => ClassName(s"scala.Function$n")) @@ -594,6 +599,8 @@ object JavalibIRCleaner { MethodName("toSeq", Nil, ClassRef(ReadOnlySeq)) private val truthValueMethodName = MethodName("truthValue", List(ClassRef(JSDynamic)), BooleanRef) + private val unionTypeFromMethodName = + MethodName("from", List(ClassRef(ObjectClass), ClassRef(UnionTypeEvidence)), ClassRef(UnionType)) private val writeReplaceMethodName = MethodName("writeReplace", Nil, ClassRef(ObjectClass)) From 7e880f132b0c6bc901f6650bd6cc5e60d9e439ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 31 May 2022 16:07:15 +0200 Subject: [PATCH 17/75] IR cleaner: Rewrite scala.MatchError to j.l.AssertionError. `AssertionError` conveniently features a constructor taking an `Object`. Since any `MatchError` in the javalib would be a bug, it is fine to rewrite them to `AssertionError`s. --- project/JavalibIRCleaner.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 4d5f22d5eb..75478d155b 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -657,7 +657,15 @@ object JavalibIRCleaner { ClassName("scala.Tuple" + n) -> ClassName("java.util.internal.Tuple" + n) } - val allPairs = functionTypePairs ++ refPairs ++ tuplePairs + val otherPairs = List( + /* AssertionError conveniently features a constructor taking an Object. + * Since any MatchError in the javalib would be a bug, it is fine to + * rewrite them to AssertionErrors. + */ + ClassName("scala.MatchError") -> ClassName("java.lang.AssertionError"), + ) + + val allPairs = functionTypePairs ++ refPairs ++ tuplePairs ++ otherPairs allPairs.toMap } } From 37ce4d1631a275107a72893fd4e33a3c2a056b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 23 Jul 2022 15:12:51 +0200 Subject: [PATCH 18/75] IR cleaner: Remove bridge methods that are superseded by the erasure. Also remove duplicate static forwarders resulting from redundant bridges. --- project/JavalibIRCleaner.scala | 85 +++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 75478d155b..809d923b7b 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -168,7 +168,7 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { validateClassName(interface.name) val transformedClassDef = - Hashers.hashClassDef(this.transformClassDef(preprocessedTree)) + Hashers.hashClassDef(eliminateRedundantBridges(this.transformClassDef(preprocessedTree))) postTransformChecks(transformedClassDef) transformedClassDef @@ -210,6 +210,89 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { } } + /** Eliminate bridges that have become redundant because of our additional erasure. */ + private def eliminateRedundantBridges(classDef: ClassDef): ClassDef = { + import MemberNamespace._ + + def argsCorrespond(args: List[Tree], paramDefs: List[ParamDef]): Boolean = { + (args.size == paramDefs.size) && args.zip(paramDefs).forall { + case (VarRef(LocalIdent(argName)), ParamDef(LocalIdent(paramName), _, _, _)) => + argName == paramName + case _ => + false + } + } + + val memberDefs = classDef.memberDefs + + // Instance bridges, which call "themselves" (another version of themselves with the same name) + + def isRedundantBridge(memberDef: MemberDef): Boolean = memberDef match { + case MethodDef(flags, MethodIdent(name), _, paramDefs, _, Some(body)) if flags.namespace == Public => + body match { + case Apply(ApplyFlags.empty, This(), MethodIdent(`name`), args) => + argsCorrespond(args, paramDefs) + case _ => + false + } + case _ => + false + } + + val newMemberDefs1 = memberDefs.filterNot(isRedundantBridge(_)) + + // Make sure that we did not remove *all* overloads for any method name + + def publicMethodNames(memberDefs: List[MemberDef]): Set[MethodName] = { + memberDefs.collect { + case MethodDef(flags, name, _, _, _, _) if flags.namespace == Public => name.name + }.toSet + } + + val lostMethodNames = publicMethodNames(memberDefs) -- publicMethodNames(newMemberDefs1) + if (lostMethodNames.nonEmpty) { + for (lostMethodName <- lostMethodNames) + reportError(s"eliminateRedundantBridges removed all overloads of ${lostMethodName.nameString}")(classDef.pos) + } + + // Static forwarders to redundant bridges -- these are duplicate public static methods + + def isStaticForwarder(memberDef: MethodDef): Boolean = memberDef match { + case MethodDef(flags, MethodIdent(name), _, paramDefs, _, Some(body)) if flags.namespace == PublicStatic => + body match { + case Apply(ApplyFlags.empty, LoadModule(_), MethodIdent(`name`), args) => + argsCorrespond(args, paramDefs) + case _ => + false + } + case _ => + false + } + + val seenStaticForwarderNames = mutable.Set.empty[MethodName] + val newMemberDefs2 = newMemberDefs1.filter { memberDef => + memberDef match { + case m: MethodDef if isStaticForwarder(m) => + seenStaticForwarderNames.add(m.name.name) // keep if it is the first one + case _ => + true // always keep + } + } + + new ClassDef( + classDef.name, + classDef.originalName, + classDef.kind, + classDef.jsClassCaptures, + classDef.superClass, + classDef.interfaces, + classDef.jsSuperClass, + classDef.jsNativeLoadSpec, + newMemberDefs2, + classDef.topLevelExportDefs + )(classDef.optimizerHints)(classDef.pos) + } + private def transformParamDefs(paramDefs: List[ParamDef]): List[ParamDef] = { for (paramDef <- paramDefs) yield { implicit val pos = paramDef.pos From 9ef2cd677231005d1f8b9662e88ba3028c021cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 1 Jun 2022 15:12:49 +0200 Subject: [PATCH 19/75] IR cleaner: Allow references to TypedArrayBufferBridge within itself. --- project/JavalibIRCleaner.scala | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 809d923b7b..8214b6ee71 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -593,7 +593,15 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { def isJavaScriptExceptionWithinItself = cls == JavaScriptExceptionClass && enclosingClassName == JavaScriptExceptionClass - if (cls.nameString.startsWith("scala.") && !isJavaScriptExceptionWithinItself) + def isTypedArrayBufferBridgeWithinItself = { + (cls == TypedArrayBufferBridge || cls == TypedArrayBufferBridgeMod) && + (enclosingClassName == TypedArrayBufferBridge || enclosingClassName == TypedArrayBufferBridgeMod) + } + + def isAnException: Boolean = + isJavaScriptExceptionWithinItself || isTypedArrayBufferBridgeWithinItself + + if (cls.nameString.startsWith("scala.") && !isAnException) reportError(s"Illegal reference to Scala class ${cls.nameString}") } @@ -623,6 +631,10 @@ object JavalibIRCleaner { // Within js.JavaScriptException, which is part of the linker private lib, we can refer to itself private val JavaScriptExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + // Within TypedArrayBufferBridge, which is actually part of the library, we can refer to itself + private val TypedArrayBufferBridge = ClassName("scala.scalajs.js.typedarray.TypedArrayBufferBridge") + private val TypedArrayBufferBridgeMod = ClassName("scala.scalajs.js.typedarray.TypedArrayBufferBridge$") + private val ImmutableSeq = ClassName("scala.collection.immutable.Seq") private val JavaIOSerializable = ClassName("java.io.Serializable") private val JSAny = ClassName("scala.scalajs.js.Any") From ab1002c492f176055893eda7aabb6db7b03bf187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 1 Jun 2022 15:09:58 +0200 Subject: [PATCH 20/75] Use signatures that do not involve JS types in TypedArrayBufferBridge. --- .../typedarray/TypedArrayBufferBridge.scala | 45 ++++++++++--------- .../js/typedarray/TypedArrayBuffer.scala | 16 +++---- .../typedarray/TypedArrayBufferBridge.scala | 29 +++++++----- .../js/typedarray/TypedArrayBufferOps.scala | 4 +- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala b/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala index c692870f9a..26d5f65ba9 100644 --- a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala +++ b/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala @@ -21,12 +21,17 @@ * members are public. * * In library/, this file has only the signatures, with stub implementations. - * In javalib/, it has the proper the proper implementations. + * In javalib/, it has the proper implementations. * The build keeps the .class coming from library/ and the .sjsir file from * javalib/. This way, we bridge the library and javalib. But that means the * binary interface of TypedArrayBufferBridge must be strictly equivalent in * the two copies. * + * Because of these copies, we must also explicitly use `Any` instead of all + * JS types in the method signatures. The IR cleaner would replace any JS type + * by `Any` in the javalib, so if we don't write them like that in the library + * as well, there will be mismatches. + * * (Yes, this is a hack.) * !!!!! */ @@ -36,45 +41,45 @@ package scala.scalajs.js.typedarray import java.nio._ private[typedarray] object TypedArrayBufferBridge { - def wrap(array: ArrayBuffer): ByteBuffer = - ByteBuffer.wrap(array) + def wrapArrayBuffer(array: Any): ByteBuffer = + ByteBuffer.wrap(array.asInstanceOf[ArrayBuffer]) - def wrap(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = - ByteBuffer.wrap(array, byteOffset, length) + def wrapArrayBuffer(array: Any, byteOffset: Int, length: Int): ByteBuffer = + ByteBuffer.wrap(array.asInstanceOf[ArrayBuffer], byteOffset, length) - def wrap(array: Int8Array): ByteBuffer = - ByteBuffer.wrap(array) + def wrapInt8Array(array: Any): ByteBuffer = + ByteBuffer.wrap(array.asInstanceOf[Int8Array]) - def wrap(array: Uint16Array): CharBuffer = - CharBuffer.wrap(array) + def wrapUint16Array(array: Any): CharBuffer = + CharBuffer.wrap(array.asInstanceOf[Uint16Array]) - def wrap(array: Int16Array): ShortBuffer = - ShortBuffer.wrap(array) + def wrapInt16Array(array: Any): ShortBuffer = + ShortBuffer.wrap(array.asInstanceOf[Int16Array]) - def wrap(array: Int32Array): IntBuffer = - IntBuffer.wrap(array) + def wrapInt32Array(array: Any): IntBuffer = + IntBuffer.wrap(array.asInstanceOf[Int32Array]) - def wrap(array: Float32Array): FloatBuffer = - FloatBuffer.wrap(array) + def wrapFloat32Array(array: Any): FloatBuffer = + FloatBuffer.wrap(array.asInstanceOf[Float32Array]) - def wrap(array: Float64Array): DoubleBuffer = - DoubleBuffer.wrap(array) + def wrapFloat64Array(array: Any): DoubleBuffer = + DoubleBuffer.wrap(array.asInstanceOf[Float64Array]) def Buffer_hasArrayBuffer(buffer: Buffer): Boolean = buffer.hasArrayBuffer() - def Buffer_arrayBuffer(buffer: Buffer): ArrayBuffer = + def Buffer_arrayBuffer(buffer: Buffer): Any = buffer.arrayBuffer() def Buffer_arrayBufferOffset(buffer: Buffer): Int = buffer.arrayBufferOffset() - def Buffer_dataView(buffer: Buffer): DataView = + def Buffer_dataView(buffer: Buffer): Any = buffer.dataView() def Buffer_hasTypedArray(buffer: Buffer): Boolean = buffer.hasTypedArray() - def Buffer_typedArray(buffer: Buffer): TypedArray[_, _] = + def Buffer_typedArray(buffer: Buffer): Any = buffer.typedArray() } diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala index 8d5319bc20..42e561d39f 100644 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala +++ b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala @@ -24,33 +24,33 @@ import java.nio._ object TypedArrayBuffer { /** Wraps an [[ArrayBuffer]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. */ def wrap(array: ArrayBuffer): ByteBuffer = - TypedArrayBufferBridge.wrap(array) + TypedArrayBufferBridge.wrapArrayBuffer(array) /** Wraps an [[ArrayBuffer]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. */ def wrap(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = - TypedArrayBufferBridge.wrap(array, byteOffset, length) + TypedArrayBufferBridge.wrapArrayBuffer(array, byteOffset, length) /** Wraps an [[Int8Array]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. */ def wrap(array: Int8Array): ByteBuffer = - TypedArrayBufferBridge.wrap(array) + TypedArrayBufferBridge.wrapInt8Array(array) /** Wraps a [[Uint16Array]] in a direct [[java.nio.CharBuffer CharBuffer]]. */ def wrap(array: Uint16Array): CharBuffer = - TypedArrayBufferBridge.wrap(array) + TypedArrayBufferBridge.wrapUint16Array(array) /** Wraps an [[Int16Array]] in a direct [[java.nio.ShortBuffer ShortBuffer]]. */ def wrap(array: Int16Array): ShortBuffer = - TypedArrayBufferBridge.wrap(array) + TypedArrayBufferBridge.wrapInt16Array(array) /** Wraps an [[Int32Array]] in a direct [[java.nio.IntBuffer IntBuffer]]. */ def wrap(array: Int32Array): IntBuffer = - TypedArrayBufferBridge.wrap(array) + TypedArrayBufferBridge.wrapInt32Array(array) /** Wraps a [[Float32Array]] in a direct [[java.nio.FloatBuffer FloatBuffer]]. */ def wrap(array: Float32Array): FloatBuffer = - TypedArrayBufferBridge.wrap(array) + TypedArrayBufferBridge.wrapFloat32Array(array) /** Wraps a [[Float64Array]] in a direct [[java.nio.DoubleBuffer DoubleBuffer]]. */ def wrap(array: Float64Array): DoubleBuffer = - TypedArrayBufferBridge.wrap(array) + TypedArrayBufferBridge.wrapFloat64Array(array) } diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala index 0f436fda7e..857abcadc4 100644 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala +++ b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala @@ -21,12 +21,17 @@ * members are public. * * In library/, this file has only the signatures, with stub implementations. - * In javalib/, it has the proper the proper implementations. + * In javalib/, it has the proper implementations. * The build keeps the .class coming from library/ and the .sjsir file from * javalib/. This way, we bridge the library and javalib. But that means the * binary interface of TypedArrayBufferBridge must be strictly equivalent in * the two copies. * + * Because of these copies, we must also explicitly use `Any` instead of all + * JS types in the method signatures. The IR cleaner would replace any JS type + * by `Any` in the javalib, so if we don't write them like that in the library + * as well, there will be mismatches. + * * (Yes, this is a hack.) * !!!!! */ @@ -36,33 +41,33 @@ package scala.scalajs.js.typedarray import java.nio._ private[typedarray] object TypedArrayBufferBridge { - def wrap(array: ArrayBuffer): ByteBuffer = stub() + def wrapArrayBuffer(array: Any): ByteBuffer = stub() - def wrap(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = stub() + def wrapArrayBuffer(array: Any, byteOffset: Int, length: Int): ByteBuffer = stub() - def wrap(array: Int8Array): ByteBuffer = stub() + def wrapInt8Array(array: Any): ByteBuffer = stub() - def wrap(array: Uint16Array): CharBuffer = stub() + def wrapUint16Array(array: Any): CharBuffer = stub() - def wrap(array: Int16Array): ShortBuffer = stub() + def wrapInt16Array(array: Any): ShortBuffer = stub() - def wrap(array: Int32Array): IntBuffer = stub() + def wrapInt32Array(array: Any): IntBuffer = stub() - def wrap(array: Float32Array): FloatBuffer = stub() + def wrapFloat32Array(array: Any): FloatBuffer = stub() - def wrap(array: Float64Array): DoubleBuffer = stub() + def wrapFloat64Array(array: Any): DoubleBuffer = stub() def Buffer_hasArrayBuffer(buffer: Buffer): Boolean = stub() - def Buffer_arrayBuffer(buffer: Buffer): ArrayBuffer = stub() + def Buffer_arrayBuffer(buffer: Buffer): Any = stub() def Buffer_arrayBufferOffset(buffer: Buffer): Int = stub() - def Buffer_dataView(buffer: Buffer): DataView = stub() + def Buffer_dataView(buffer: Buffer): Any = stub() def Buffer_hasTypedArray(buffer: Buffer): Boolean = stub() - def Buffer_typedArray(buffer: Buffer): TypedArray[_, _] = stub() + def Buffer_typedArray(buffer: Buffer): Any = stub() private def stub(): Nothing = throw new Error("stub") diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala index 9572727d9e..b0c1911a0c 100644 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala +++ b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala @@ -41,7 +41,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * If this buffer has no backing [[ArrayBuffer]], i.e., !hasArrayBuffer() */ def arrayBuffer(): ArrayBuffer = - TypedArrayBufferBridge.Buffer_arrayBuffer(buffer) + TypedArrayBufferBridge.Buffer_arrayBuffer(buffer).asInstanceOf[ArrayBuffer] /** Byte offset in the associated [[ArrayBuffer]] _(optional operation)_. * @@ -60,7 +60,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * If this buffer has no backing [[ArrayBuffer]], i.e., !hasArrayBuffer() */ def dataView(): DataView = - TypedArrayBufferBridge.Buffer_dataView(buffer) + TypedArrayBufferBridge.Buffer_dataView(buffer).asInstanceOf[DataView] /** Tests whether this direct buffer has a valid associated [[TypedArray]]. * From 08fe64869f176756b16d0a4f40c8cbfe81fe56b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 1 Jun 2022 15:38:25 +0200 Subject: [PATCH 21/75] Give more specific names to the Buffer extended API's `wrap` methods. This is to avoid duplicate definitions after the "erasure" performed by the IR cleaner. --- javalib/src/main/scala/java/nio/ByteBuffer.scala | 12 ++++++------ javalib/src/main/scala/java/nio/CharBuffer.scala | 4 ++-- .../src/main/scala/java/nio/DoubleBuffer.scala | 4 ++-- .../src/main/scala/java/nio/FloatBuffer.scala | 4 ++-- javalib/src/main/scala/java/nio/IntBuffer.scala | 4 ++-- .../src/main/scala/java/nio/ShortBuffer.scala | 4 ++-- .../scala/java/nio/TypedArrayByteBuffer.scala | 10 +++++----- .../scala/java/nio/TypedArrayCharBuffer.scala | 2 +- .../scala/java/nio/TypedArrayDoubleBuffer.scala | 2 +- .../scala/java/nio/TypedArrayFloatBuffer.scala | 2 +- .../scala/java/nio/TypedArrayIntBuffer.scala | 2 +- .../scala/java/nio/TypedArrayShortBuffer.scala | 2 +- .../js/typedarray/TypedArrayBufferBridge.scala | 16 ++++++++-------- 13 files changed, 34 insertions(+), 34 deletions(-) diff --git a/javalib/src/main/scala/java/nio/ByteBuffer.scala b/javalib/src/main/scala/java/nio/ByteBuffer.scala index 8b100204f8..92cb2a8ea0 100644 --- a/javalib/src/main/scala/java/nio/ByteBuffer.scala +++ b/javalib/src/main/scala/java/nio/ByteBuffer.scala @@ -31,14 +31,14 @@ object ByteBuffer { // Extended API - def wrap(array: ArrayBuffer): ByteBuffer = - TypedArrayByteBuffer.wrap(array) + def wrapArrayBuffer(array: ArrayBuffer): ByteBuffer = + TypedArrayByteBuffer.wrapArrayBuffer(array) - def wrap(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = - TypedArrayByteBuffer.wrap(array, byteOffset, length) + def wrapArrayBuffer(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = + TypedArrayByteBuffer.wrapArrayBuffer(array, byteOffset, length) - def wrap(array: Int8Array): ByteBuffer = - TypedArrayByteBuffer.wrap(array) + def wrapInt8Array(array: Int8Array): ByteBuffer = + TypedArrayByteBuffer.wrapInt8Array(array) } abstract class ByteBuffer private[nio] ( diff --git a/javalib/src/main/scala/java/nio/CharBuffer.scala b/javalib/src/main/scala/java/nio/CharBuffer.scala index 79443286ec..8501f7a01c 100644 --- a/javalib/src/main/scala/java/nio/CharBuffer.scala +++ b/javalib/src/main/scala/java/nio/CharBuffer.scala @@ -34,8 +34,8 @@ object CharBuffer { // Extended API - def wrap(array: Uint16Array): CharBuffer = - TypedArrayCharBuffer.wrap(array) + def wrapUint16Array(array: Uint16Array): CharBuffer = + TypedArrayCharBuffer.wrapUint16Array(array) } abstract class CharBuffer private[nio] ( diff --git a/javalib/src/main/scala/java/nio/DoubleBuffer.scala b/javalib/src/main/scala/java/nio/DoubleBuffer.scala index 34c77ba0c5..4a4fcda944 100644 --- a/javalib/src/main/scala/java/nio/DoubleBuffer.scala +++ b/javalib/src/main/scala/java/nio/DoubleBuffer.scala @@ -28,8 +28,8 @@ object DoubleBuffer { // Extended API - def wrap(array: Float64Array): DoubleBuffer = - TypedArrayDoubleBuffer.wrap(array) + def wrapFloat64Array(array: Float64Array): DoubleBuffer = + TypedArrayDoubleBuffer.wrapFloat64Array(array) } abstract class DoubleBuffer private[nio] ( diff --git a/javalib/src/main/scala/java/nio/FloatBuffer.scala b/javalib/src/main/scala/java/nio/FloatBuffer.scala index dc816242c6..9f9a8021de 100644 --- a/javalib/src/main/scala/java/nio/FloatBuffer.scala +++ b/javalib/src/main/scala/java/nio/FloatBuffer.scala @@ -28,8 +28,8 @@ object FloatBuffer { // Extended API - def wrap(array: Float32Array): FloatBuffer = - TypedArrayFloatBuffer.wrap(array) + def wrapFloat32Array(array: Float32Array): FloatBuffer = + TypedArrayFloatBuffer.wrapFloat32Array(array) } abstract class FloatBuffer private[nio] ( diff --git a/javalib/src/main/scala/java/nio/IntBuffer.scala b/javalib/src/main/scala/java/nio/IntBuffer.scala index 09cfa88515..5e31304b4f 100644 --- a/javalib/src/main/scala/java/nio/IntBuffer.scala +++ b/javalib/src/main/scala/java/nio/IntBuffer.scala @@ -28,8 +28,8 @@ object IntBuffer { // Extended API - def wrap(array: Int32Array): IntBuffer = - TypedArrayIntBuffer.wrap(array) + def wrapInt32Array(array: Int32Array): IntBuffer = + TypedArrayIntBuffer.wrapInt32Array(array) } abstract class IntBuffer private[nio] ( diff --git a/javalib/src/main/scala/java/nio/ShortBuffer.scala b/javalib/src/main/scala/java/nio/ShortBuffer.scala index d31b13fec8..b3d3b9b0b5 100644 --- a/javalib/src/main/scala/java/nio/ShortBuffer.scala +++ b/javalib/src/main/scala/java/nio/ShortBuffer.scala @@ -28,8 +28,8 @@ object ShortBuffer { // Extended API - def wrap(array: Int16Array): ShortBuffer = - TypedArrayShortBuffer.wrap(array) + def wrapInt16Array(array: Int16Array): ShortBuffer = + TypedArrayShortBuffer.wrapInt16Array(array) } abstract class ShortBuffer private[nio] ( diff --git a/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala index 26f93b0012..1debeb3208 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala @@ -225,13 +225,13 @@ private[nio] object TypedArrayByteBuffer { new TypedArrayByteBuffer(new Int8Array(capacity), 0, capacity, false) } - def wrap(array: ArrayBuffer): ByteBuffer = - wrap(new Int8Array(array)) + def wrapArrayBuffer(array: ArrayBuffer): ByteBuffer = + wrapInt8Array(new Int8Array(array)) - def wrap(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = - wrap(new Int8Array(array, byteOffset, length)) + def wrapArrayBuffer(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = + wrapInt8Array(new Int8Array(array, byteOffset, length)) - def wrap(typedArray: Int8Array): ByteBuffer = { + def wrapInt8Array(typedArray: Int8Array): ByteBuffer = { val buf = new TypedArrayByteBuffer(typedArray, 0, typedArray.length, false) buf._isBigEndian = ByteOrder.areTypedArraysBigEndian buf diff --git a/javalib/src/main/scala/java/nio/TypedArrayCharBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayCharBuffer.scala index 96a8d82056..71a51057d2 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayCharBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayCharBuffer.scala @@ -135,6 +135,6 @@ private[nio] object TypedArrayCharBuffer { def fromTypedArrayByteBuffer(byteBuffer: TypedArrayByteBuffer): CharBuffer = GenTypedArrayBuffer.generic_fromTypedArrayByteBuffer(byteBuffer) - def wrap(array: Uint16Array): CharBuffer = + def wrapUint16Array(array: Uint16Array): CharBuffer = new TypedArrayCharBuffer(array, 0, array.length, false) } diff --git a/javalib/src/main/scala/java/nio/TypedArrayDoubleBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayDoubleBuffer.scala index 5cb48beace..4211fb143b 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayDoubleBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayDoubleBuffer.scala @@ -128,6 +128,6 @@ private[nio] object TypedArrayDoubleBuffer { def fromTypedArrayByteBuffer(byteBuffer: TypedArrayByteBuffer): DoubleBuffer = GenTypedArrayBuffer.generic_fromTypedArrayByteBuffer(byteBuffer) - def wrap(array: Float64Array): DoubleBuffer = + def wrapFloat64Array(array: Float64Array): DoubleBuffer = new TypedArrayDoubleBuffer(array, 0, array.length, false) } diff --git a/javalib/src/main/scala/java/nio/TypedArrayFloatBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayFloatBuffer.scala index d485e87054..cab3cbc756 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayFloatBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayFloatBuffer.scala @@ -128,6 +128,6 @@ private[nio] object TypedArrayFloatBuffer { def fromTypedArrayByteBuffer(byteBuffer: TypedArrayByteBuffer): FloatBuffer = GenTypedArrayBuffer.generic_fromTypedArrayByteBuffer(byteBuffer) - def wrap(array: Float32Array): FloatBuffer = + def wrapFloat32Array(array: Float32Array): FloatBuffer = new TypedArrayFloatBuffer(array, 0, array.length, false) } diff --git a/javalib/src/main/scala/java/nio/TypedArrayIntBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayIntBuffer.scala index 2d73e5025e..8beab4ac58 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayIntBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayIntBuffer.scala @@ -128,6 +128,6 @@ private[nio] object TypedArrayIntBuffer { def fromTypedArrayByteBuffer(byteBuffer: TypedArrayByteBuffer): IntBuffer = GenTypedArrayBuffer.generic_fromTypedArrayByteBuffer(byteBuffer) - def wrap(array: Int32Array): IntBuffer = + def wrapInt32Array(array: Int32Array): IntBuffer = new TypedArrayIntBuffer(array, 0, array.length, false) } diff --git a/javalib/src/main/scala/java/nio/TypedArrayShortBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayShortBuffer.scala index 0c77246b34..09a9ca38dc 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayShortBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayShortBuffer.scala @@ -128,6 +128,6 @@ private[nio] object TypedArrayShortBuffer { def fromTypedArrayByteBuffer(byteBuffer: TypedArrayByteBuffer): ShortBuffer = GenTypedArrayBuffer.generic_fromTypedArrayByteBuffer(byteBuffer) - def wrap(array: Int16Array): ShortBuffer = + def wrapInt16Array(array: Int16Array): ShortBuffer = new TypedArrayShortBuffer(array, 0, array.length, false) } diff --git a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala b/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala index 26d5f65ba9..3468a9f7e9 100644 --- a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala +++ b/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala @@ -42,28 +42,28 @@ import java.nio._ private[typedarray] object TypedArrayBufferBridge { def wrapArrayBuffer(array: Any): ByteBuffer = - ByteBuffer.wrap(array.asInstanceOf[ArrayBuffer]) + ByteBuffer.wrapArrayBuffer(array.asInstanceOf[ArrayBuffer]) def wrapArrayBuffer(array: Any, byteOffset: Int, length: Int): ByteBuffer = - ByteBuffer.wrap(array.asInstanceOf[ArrayBuffer], byteOffset, length) + ByteBuffer.wrapArrayBuffer(array.asInstanceOf[ArrayBuffer], byteOffset, length) def wrapInt8Array(array: Any): ByteBuffer = - ByteBuffer.wrap(array.asInstanceOf[Int8Array]) + ByteBuffer.wrapInt8Array(array.asInstanceOf[Int8Array]) def wrapUint16Array(array: Any): CharBuffer = - CharBuffer.wrap(array.asInstanceOf[Uint16Array]) + CharBuffer.wrapUint16Array(array.asInstanceOf[Uint16Array]) def wrapInt16Array(array: Any): ShortBuffer = - ShortBuffer.wrap(array.asInstanceOf[Int16Array]) + ShortBuffer.wrapInt16Array(array.asInstanceOf[Int16Array]) def wrapInt32Array(array: Any): IntBuffer = - IntBuffer.wrap(array.asInstanceOf[Int32Array]) + IntBuffer.wrapInt32Array(array.asInstanceOf[Int32Array]) def wrapFloat32Array(array: Any): FloatBuffer = - FloatBuffer.wrap(array.asInstanceOf[Float32Array]) + FloatBuffer.wrapFloat32Array(array.asInstanceOf[Float32Array]) def wrapFloat64Array(array: Any): DoubleBuffer = - DoubleBuffer.wrap(array.asInstanceOf[Float64Array]) + DoubleBuffer.wrapFloat64Array(array.asInstanceOf[Float64Array]) def Buffer_hasArrayBuffer(buffer: Buffer): Boolean = buffer.hasArrayBuffer() From cbc358e5b3ba7f012eefbdb9b00c9d363194e8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 9 Dec 2021 11:44:09 +0100 Subject: [PATCH 22/75] Use an internal copy of MurmurHash3 in the javalib. --- javalib/src/main/scala/java/net/URI.scala | 2 +- .../src/main/scala/java/nio/GenBuffer.scala | 2 +- .../java/util/internal/MurmurHash3.scala | 61 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 javalib/src/main/scala/java/util/internal/MurmurHash3.scala diff --git a/javalib/src/main/scala/java/net/URI.scala b/javalib/src/main/scala/java/net/URI.scala index 58dda9f946..3b41312f72 100644 --- a/javalib/src/main/scala/java/net/URI.scala +++ b/javalib/src/main/scala/java/net/URI.scala @@ -187,7 +187,7 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] { def getUserInfo(): String = decodeComponent(_userInfo) override def hashCode(): Int = { - import scala.util.hashing.MurmurHash3._ + import java.util.internal.MurmurHash3._ import URI.normalizeEscapes def normalizeEscapesHash(str: String): Int = diff --git a/javalib/src/main/scala/java/nio/GenBuffer.scala b/javalib/src/main/scala/java/nio/GenBuffer.scala index 9489f579a3..514a0daec9 100644 --- a/javalib/src/main/scala/java/nio/GenBuffer.scala +++ b/javalib/src/main/scala/java/nio/GenBuffer.scala @@ -118,7 +118,7 @@ private[nio] final class GenBuffer[B <: Buffer] private (val self: B) @inline def generic_hashCode(hashSeed: Int): Int = { - import scala.util.hashing.MurmurHash3._ + import java.util.internal.MurmurHash3._ val start = position() val end = limit() var h = hashSeed diff --git a/javalib/src/main/scala/java/util/internal/MurmurHash3.scala b/javalib/src/main/scala/java/util/internal/MurmurHash3.scala new file mode 100644 index 0000000000..bcf438f131 --- /dev/null +++ b/javalib/src/main/scala/java/util/internal/MurmurHash3.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 java.util.internal + +import java.lang.Integer.{rotateLeft => rotl} + +/** Primitives to implement MurmurHash3 hashes in data structures. + * + * This is copy of parts of `scala.util.hashing.MurmurHash3`. + */ +private[java] object MurmurHash3 { + /** Mix in a block of data into an intermediate hash value. */ + final def mix(hash: Int, data: Int): Int = { + var h = mixLast(hash, data) + h = rotl(h, 13) + h * 5 + 0xe6546b64 + } + + /** May optionally be used as the last mixing step. + * + * Is a little bit faster than mix, as it does no further mixing of the + * resulting hash. For the last element this is not necessary as the hash is + * thoroughly mixed during finalization anyway. + */ + final def mixLast(hash: Int, data: Int): Int = { + var k = data + + k *= 0xcc9e2d51 + k = rotl(k, 15) + k *= 0x1b873593 + + hash ^ k + } + + /** Finalize a hash to incorporate the length and make sure all bits avalanche. */ + @noinline final def finalizeHash(hash: Int, length: Int): Int = + avalanche(hash ^ length) + + /** Force all bits of the hash to avalanche. Used for finalizing the hash. */ + @inline private final def avalanche(hash: Int): Int = { + var h = hash + + h ^= h >>> 16 + h *= 0x85ebca6b + h ^= h >>> 13 + h *= 0xc2b2ae35 + h ^= h >>> 16 + + h + } +} From c7dc0e1b8dc80416c8679bb0db82893f5ad6bde8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 1 Jun 2022 14:46:13 +0200 Subject: [PATCH 23/75] Use an internal copy of the features of DataViewExt in java.nio. --- .../src/main/scala/java/nio/DataViewExt.scala | 46 +++++++++++++++++++ .../scala/java/nio/DataViewLongBuffer.scala | 7 +-- .../scala/java/nio/TypedArrayByteBuffer.scala | 11 +++-- 3 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 javalib/src/main/scala/java/nio/DataViewExt.scala diff --git a/javalib/src/main/scala/java/nio/DataViewExt.scala b/javalib/src/main/scala/java/nio/DataViewExt.scala new file mode 100644 index 0000000000..f034f2f915 --- /dev/null +++ b/javalib/src/main/scala/java/nio/DataViewExt.scala @@ -0,0 +1,46 @@ +/* + * 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.nio + +import scala.scalajs.js.typedarray.DataView + +/** Copy of features in `scala.scalajs.js.typedarray.DateViewExt`. + * + * Defined as functions instead of extension methods, because the AnyVal over + * a JS type generates an `equals` method that references `BoxesRunTime`. + */ +private[nio] object DataViewExt { + /** Reads a 2's complement signed 64-bit integers from the data view. + * @param index Starting index + * @param littleEndian Whether the number is stored in little endian + */ + @inline + def dataViewGetInt64(dataView: DataView, index: Int, littleEndian: Boolean): Long = { + val high = dataView.getInt32(index + (if (littleEndian) 4 else 0), littleEndian) + val low = dataView.getInt32(index + (if (littleEndian) 0 else 4), littleEndian) + (high.toLong << 32) | (low.toLong & 0xffffffffL) + } + + /** Writes a 2's complement signed 64-bit integers to the data view. + * @param index Starting index + * @param value Value to be written + * @param littleEndian Whether to store the number in little endian + */ + @inline + def dataViewSetInt64(dataView: DataView, index: Int, value: Long, littleEndian: Boolean): Unit = { + val high = (value >>> 32).toInt + val low = value.toInt + dataView.setInt32(index + (if (littleEndian) 4 else 0), high, littleEndian) + dataView.setInt32(index + (if (littleEndian) 0 else 4), low, littleEndian) + } +} diff --git a/javalib/src/main/scala/java/nio/DataViewLongBuffer.scala b/javalib/src/main/scala/java/nio/DataViewLongBuffer.scala index 3ee08fee13..3d083001cb 100644 --- a/javalib/src/main/scala/java/nio/DataViewLongBuffer.scala +++ b/javalib/src/main/scala/java/nio/DataViewLongBuffer.scala @@ -12,8 +12,9 @@ package java.nio +import java.nio.DataViewExt._ + import scala.scalajs.js.typedarray._ -import DataViewExt._ private[nio] final class DataViewLongBuffer private ( override private[nio] val _dataView: DataView, @@ -86,11 +87,11 @@ private[nio] final class DataViewLongBuffer private ( @inline private[nio] def load(index: Int): Long = - _dataView.getInt64(8 * index, !isBigEndian) + dataViewGetInt64(_dataView, 8 * index, !isBigEndian) @inline private[nio] def store(index: Int, elem: Long): Unit = - _dataView.setInt64(8 * index, elem, !isBigEndian) + dataViewSetInt64(_dataView, 8 * index, elem, !isBigEndian) @inline override private[nio] def load(startIndex: Int, diff --git a/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala index 1debeb3208..2371a887e5 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala @@ -12,8 +12,9 @@ package java.nio +import java.nio.DataViewExt._ + import scala.scalajs.js.typedarray._ -import DataViewExt._ private[nio] final class TypedArrayByteBuffer private ( override private[nio] val _typedArray: Int8Array, @@ -128,13 +129,13 @@ private[nio] final class TypedArrayByteBuffer private ( } @noinline def getLong(): Long = - _dataView.getInt64(getPosAndAdvanceRead(8), !isBigEndian) + dataViewGetInt64(_dataView, getPosAndAdvanceRead(8), !isBigEndian) @noinline def putLong(value: Long): ByteBuffer = - { ensureNotReadOnly(); _dataView.setInt64(getPosAndAdvanceWrite(8), value, !isBigEndian); this } + { ensureNotReadOnly(); dataViewSetInt64(_dataView, getPosAndAdvanceWrite(8), value, !isBigEndian); this } @noinline def getLong(index: Int): Long = - _dataView.getInt64(validateIndex(index, 8), !isBigEndian) + dataViewGetInt64(_dataView, validateIndex(index, 8), !isBigEndian) @noinline def putLong(index: Int, value: Long): ByteBuffer = - { ensureNotReadOnly(); _dataView.setInt64(validateIndex(index, 8), value, !isBigEndian); this } + { ensureNotReadOnly(); dataViewSetInt64(_dataView, validateIndex(index, 8), value, !isBigEndian); this } def asLongBuffer(): LongBuffer = DataViewLongBuffer.fromTypedArrayByteBuffer(this) From 9dee653c53e041f19381a290730b2e37da1ec903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 2 Jul 2022 15:46:00 +0200 Subject: [PATCH 24/75] Turn `find` into `findFold` in ScalaOps. This is a fused operation of `find` and `fold`, which avoids the intermediate `Option`. --- javalib/src/main/scala/java/util/AbstractMap.scala | 2 +- javalib/src/main/scala/java/util/ScalaOps.scala | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/javalib/src/main/scala/java/util/AbstractMap.scala b/javalib/src/main/scala/java/util/AbstractMap.scala index d2ea01b065..df75f5ba67 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.find(entry => Objects.equals(key, entry.getKey())).fold[V] { + entrySet().scalaOps.findFold(entry => Objects.equals(key, entry.getKey())) { null.asInstanceOf[V] } { entry => entry.getValue() diff --git a/javalib/src/main/scala/java/util/ScalaOps.scala b/javalib/src/main/scala/java/util/ScalaOps.scala index 4362f77fa8..a3c64920b3 100644 --- a/javalib/src/main/scala/java/util/ScalaOps.scala +++ b/javalib/src/main/scala/java/util/ScalaOps.scala @@ -57,8 +57,8 @@ private[java] object ScalaOps { @inline def indexWhere(f: A => Boolean): Int = __self.iterator().scalaOps.indexWhere(f) - @inline def find(f: A => Boolean): Option[A] = - __self.iterator().scalaOps.find(f) + @inline def findFold[B](f: A => Boolean)(default: => B)(g: A => B): B = + __self.iterator().scalaOps.findFold(f)(default)(g) @inline def foldLeft[B](z: B)(f: (B, A) => B): B = __self.iterator().scalaOps.foldLeft(z)(f) @@ -112,14 +112,14 @@ private[java] object ScalaOps { // scalastyle:on return } - @inline def find(f: A => Boolean): Option[A] = { + @inline def findFold[B](f: A => Boolean)(default: => B)(g: A => B): B = { // scalastyle:off return while (__self.hasNext()) { val x = __self.next() if (f(x)) - return Some(x) + return g(x) } - None + default // scalastyle:on return } From 7d9b14dd36c56adf7aad985d494c3e4cc631b802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 2 Jul 2022 15:53:04 +0200 Subject: [PATCH 25/75] Avoid a vararg in an internal method of BigDecimal. --- .../src/main/scala/java/math/BigDecimal.scala | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/javalib/src/main/scala/java/math/BigDecimal.scala b/javalib/src/main/scala/java/math/BigDecimal.scala index c3ffdba757..4f328accea 100644 --- a/javalib/src/main/scala/java/math/BigDecimal.scala +++ b/javalib/src/main/scala/java/math/BigDecimal.scala @@ -276,11 +276,20 @@ object BigDecimal { 32 - java.lang.Integer.numberOfLeadingZeros(smallValue) } - @inline - private def charNotEqualTo(c: Char, cs: Char*): Boolean = !cs.contains(c) + private def charNotEqualTo(c: Char, cs: Array[Char]): Boolean = !charEqualTo(c, cs) - @inline - private def charEqualTo(c: Char, cs: Char*): Boolean = cs.contains(c) + private def charEqualTo(c: Char, cs: Array[Char]): Boolean = { + // scalastyle:off return + val len = cs.length + var i = 0 + while (i != len) { + if (cs(i) == c) + return true + i += 1 + } + false + // scalastyle:on return + } @inline private def insertString(s: String, pos: Int, s2: String): String = @@ -374,12 +383,12 @@ class BigDecimal() extends Number with Comparable[BigDecimal] { if (offset <= last && in(offset) == '+') { index += 1 // Fail if the next character is another sign. - if (index < last && charEqualTo(in(index), '+', '-')) + if (index < last && charEqualTo(in(index), Array('+', '-'))) throw new NumberFormatException("For input string: " + in.toString) } else { // check that '-' is not followed by another sign val isMinus = index <= last && in(index) == '-' - val nextIsSign = index + 1 < last && charEqualTo(in(index + 1), '+', '-') + val nextIsSign = index + 1 < last && charEqualTo(in(index + 1), Array('+', '-')) if (isMinus && nextIsSign) throw new NumberFormatException("For input string: " + in.toString) } @@ -388,7 +397,7 @@ class BigDecimal() extends Number with Comparable[BigDecimal] { var counter = 0 var wasNonZero = false // Accumulating all digits until a possible decimal point - while (index <= last && charNotEqualTo(in(index), '.', 'e', 'E')) { + while (index <= last && charNotEqualTo(in(index), Array('.', 'e', 'E'))) { if (!wasNonZero) { if (in(index) == '0') counter += 1 else wasNonZero = true @@ -404,7 +413,7 @@ class BigDecimal() extends Number with Comparable[BigDecimal] { index += 1 // Accumulating all digits until a possible exponent val begin = index - while (index <= last && charNotEqualTo(in(index), 'e', 'E')) { + while (index <= last && charNotEqualTo(in(index), Array('e', 'E'))) { if (!wasNonZero) { if (in(index) == '0') counter += 1 else wasNonZero = true @@ -420,7 +429,7 @@ class BigDecimal() extends Number with Comparable[BigDecimal] { } // An exponent was found - if ((index <= last) && charEqualTo(in(index), 'e', 'E')) { + if ((index <= last) && charEqualTo(in(index), Array('e', 'E'))) { index += 1 // Checking for a possible sign of scale val indexIsPlus = index <= last && in(index) == '+' From cbe00b116f0ffda90d812e31a9d35ad42571ee3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 2 Jul 2022 15:55:30 +0200 Subject: [PATCH 26/75] Avoid generic operations on scala.Arrays in java.math. Generic operations manipulate ClassTags, which we cannot depend on. --- .../src/main/scala/java/math/BigDecimal.scala | 43 ++++++++++++++----- .../src/main/scala/java/math/BigInteger.scala | 7 ++- .../main/scala/java/math/Multiplication.scala | 9 +++- .../src/main/scala/java/math/Primality.scala | 9 +++- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/javalib/src/main/scala/java/math/BigDecimal.scala b/javalib/src/main/scala/java/math/BigDecimal.scala index 4f328accea..6ee2f36636 100644 --- a/javalib/src/main/scala/java/math/BigDecimal.scala +++ b/javalib/src/main/scala/java/math/BigDecimal.scala @@ -58,8 +58,13 @@ object BigDecimal { private final val LongFivePows = newArrayOfPows(28, 5) - private final val LongFivePowsBitLength = - Array.tabulate[Int](LongFivePows.length)(i => bitLength(LongFivePows(i))) + private final val LongFivePowsBitLength = { + val len = LongFivePows.length + val result = new Array[Int](len) + for (i <- 0 until len) + result(i) = bitLength(LongFivePows(i)) + result + } /** An array of longs with powers of ten. * @@ -68,8 +73,13 @@ object BigDecimal { */ private[math] final val LongTenPows = newArrayOfPows(19, 10) - private final val LongTenPowsBitLength = - Array.tabulate[Int](LongTenPows.length)(i => bitLength(LongTenPows(i))) + private final val LongTenPowsBitLength = { + val len = LongTenPows.length + val result = new Array[Int](len) + for (i <- 0 until len) + result(i) = bitLength(LongTenPows(i)) + result + } private final val BigIntScaledByZeroLength = 11 @@ -77,15 +87,23 @@ object BigDecimal { * * ([0,0],[1,0],...,[10,0]). */ - private final val BigIntScaledByZero = - Array.tabulate[BigDecimal](BigIntScaledByZeroLength)(new BigDecimal(_, 0)) + private final val BigIntScaledByZero = { + val result = new Array[BigDecimal](BigIntScaledByZeroLength) + for (i <- 0 until BigIntScaledByZeroLength) + result(i) = new BigDecimal(i, 0) + result + } /** An array with the zero number scaled by the first positive scales. * * (0*10^0, 0*10^1, ..., 0*10^10). */ - private final val ZeroScaledBy = - Array.tabulate[BigDecimal](BigIntScaledByZeroLength)(new BigDecimal(0, _)) + private final val ZeroScaledBy = { + val result = new Array[BigDecimal](BigIntScaledByZeroLength) + for (i <- 0 until BigIntScaledByZeroLength) + result(i) = new BigDecimal(0, i) + result + } /** A string filled with 100 times the character `'0'`. * It is not a `final` val so that it isn't copied at every call site. @@ -205,8 +223,13 @@ object BigDecimal { else 0 } - private[math] def newArrayOfPows(len: Int, pow: Int): Array[Long] = - Array.iterate(1L, len)(_ * pow) + private[math] def newArrayOfPows(len: Int, pow: Int): Array[Long] = { + val result = new Array[Long](len) + result(0) = 1L + for (i <- 1 until len) + result(i) = result(i - 1) * pow + result + } /** Return an increment that can be -1,0 or 1, depending on {@code roundingMode}. * diff --git a/javalib/src/main/scala/java/math/BigInteger.scala b/javalib/src/main/scala/java/math/BigInteger.scala index 2de2c425cb..59ce0d1c49 100644 --- a/javalib/src/main/scala/java/math/BigInteger.scala +++ b/javalib/src/main/scala/java/math/BigInteger.scala @@ -74,7 +74,12 @@ object BigInteger { new BigInteger(1, 4), new BigInteger(1, 5), new BigInteger(1, 6), new BigInteger(1, 7), new BigInteger(1, 8), new BigInteger(1, 9), TEN) - private final val TWO_POWS = Array.tabulate[BigInteger](32)(i => BigInteger.valueOf(1L << i)) + private final val TWO_POWS = { + val result = new Array[BigInteger](32) + for (i <- 0 until 32) + result(i) = BigInteger.valueOf(1L << i) + result + } /** The first non zero digit is either -1 if sign is zero, otherwise it is >= 0. * diff --git a/javalib/src/main/scala/java/math/Multiplication.scala b/javalib/src/main/scala/java/math/Multiplication.scala index 9f0ca4188e..859d9f926f 100644 --- a/javalib/src/main/scala/java/math/Multiplication.scala +++ b/javalib/src/main/scala/java/math/Multiplication.scala @@ -449,6 +449,11 @@ private[math] object Multiplication { } } - private def newArrayOfPows(len: Int, pow: Int): Array[Int] = - Array.iterate(1, len)(_ * pow) + private def newArrayOfPows(len: Int, pow: Int): Array[Int] = { + val result = new Array[Int](len) + result(0) = 1 + for (i <- 1 until len) + result(i) = result(i - 1) * pow + result + } } diff --git a/javalib/src/main/scala/java/math/Primality.scala b/javalib/src/main/scala/java/math/Primality.scala index 354b6607c9..b7fd19101b 100644 --- a/javalib/src/main/scala/java/math/Primality.scala +++ b/javalib/src/main/scala/java/math/Primality.scala @@ -79,8 +79,13 @@ private[math] object Primality { (18, 13), (31, 23), (54, 43), (97, 75)) /** All {@code BigInteger} prime numbers with bit length lesser than 8 bits. */ - private val BiPrimes = - Array.tabulate[BigInteger](Primes.length)(i => BigInteger.valueOf(Primes(i))) + private val BiPrimes = { + val len = Primes.length + val result = new Array[BigInteger](len) + for (i <- 0 until len) + result(i) = BigInteger.valueOf(Primes(i)) + result + } /** A random number is generated until a probable prime number is found. * From 0c1dca7c438bb27f5e28d2c4e40d41be3c213240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 2 Jul 2022 15:59:50 +0200 Subject: [PATCH 27/75] Avoid references to run-time features of scalajs-library in the javalib. --- javalib/src/main/scala/java/net/URI.scala | 7 +- .../main/scala/java/nio/charset/Charset.scala | 19 +- .../scala/java/nio/charset/CoderResult.scala | 5 +- .../src/main/scala/java/util/ArrayDeque.scala | 83 +++++--- .../src/main/scala/java/util/ArrayList.scala | 9 +- .../src/main/scala/java/util/Formatter.scala | 5 +- .../src/main/scala/java/util/JSUtils.scala | 182 +++++++++++++++++ .../src/main/scala/java/util/TimerTask.scala | 5 +- .../concurrent/CopyOnWriteArrayList.scala | 5 +- .../java/util/regex/IndicesBuilder.scala | 16 +- .../main/scala/java/util/regex/Matcher.scala | 20 +- .../main/scala/java/util/regex/Pattern.scala | 7 +- .../java/util/regex/PatternCompiler.scala | 188 +++++++++--------- .../util/regex/PatternSyntaxException.scala | 3 +- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- 15 files changed, 389 insertions(+), 171 deletions(-) create mode 100644 javalib/src/main/scala/java/util/JSUtils.scala diff --git a/javalib/src/main/scala/java/net/URI.scala b/javalib/src/main/scala/java/net/URI.scala index 3b41312f72..e114fe6ac7 100644 --- a/javalib/src/main/scala/java/net/URI.scala +++ b/javalib/src/main/scala/java/net/URI.scala @@ -19,6 +19,7 @@ import scala.annotation.tailrec import java.nio._ import java.nio.charset.{CodingErrorAction, StandardCharsets} +import java.util.JSUtils._ final class URI(origStr: String) extends Serializable with Comparable[URI] { @@ -35,10 +36,10 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] { if (_fld == null) throw new URISyntaxException(origStr, "Malformed URI") - private val _isAbsolute = _fld(AbsScheme).isDefined - private val _isOpaque = _fld(AbsOpaquePart).isDefined + private val _isAbsolute = undefOrIsDefined(_fld(AbsScheme)) + private val _isOpaque = undefOrIsDefined(_fld(AbsOpaquePart)) - @inline private def fld(idx: Int): String = _fld(idx).orNull + @inline private def fld(idx: Int): String = undefOrGetOrNull(_fld(idx)) @inline private def fld(absIdx: Int, relIdx: Int): String = if (_isAbsolute) fld(absIdx) else fld(relIdx) diff --git a/javalib/src/main/scala/java/nio/charset/Charset.scala b/javalib/src/main/scala/java/nio/charset/Charset.scala index 2edce1ae00..c053e242ba 100644 --- a/javalib/src/main/scala/java/nio/charset/Charset.scala +++ b/javalib/src/main/scala/java/nio/charset/Charset.scala @@ -15,6 +15,7 @@ package java.nio.charset import java.nio.{ByteBuffer, CharBuffer} import java.util.{Collections, HashSet, Arrays} import java.util.ScalaOps._ +import java.util.JSUtils._ import scala.scalajs.js @@ -78,20 +79,22 @@ object Charset { def defaultCharset(): Charset = UTF_8 - def forName(charsetName: String): Charset = - CharsetMap.getOrElse(charsetName.toLowerCase, - throw new UnsupportedCharsetException(charsetName)) + def forName(charsetName: String): Charset = { + dictGetOrElse(CharsetMap, charsetName.toLowerCase()) { + throw new UnsupportedCharsetException(charsetName) + } + } def isSupported(charsetName: String): Boolean = - CharsetMap.contains(charsetName.toLowerCase) + dictContains(CharsetMap, charsetName.toLowerCase()) private lazy val CharsetMap = { - val m = js.Dictionary.empty[Charset] - for (c <- js.Array(US_ASCII, ISO_8859_1, UTF_8, UTF_16BE, UTF_16LE, UTF_16)) { - m(c.name().toLowerCase) = c + val m = dictEmpty[Charset]() + forArrayElems(js.Array(US_ASCII, ISO_8859_1, UTF_8, UTF_16BE, UTF_16LE, UTF_16)) { c => + dictSet(m, c.name().toLowerCase(), c) val aliases = c._aliases for (i <- 0 until aliases.length) - m(aliases(i).toLowerCase) = c + dictSet(m, aliases(i).toLowerCase(), c) } m } diff --git a/javalib/src/main/scala/java/nio/charset/CoderResult.scala b/javalib/src/main/scala/java/nio/charset/CoderResult.scala index f7f73967f3..257dd0904b 100644 --- a/javalib/src/main/scala/java/nio/charset/CoderResult.scala +++ b/javalib/src/main/scala/java/nio/charset/CoderResult.scala @@ -15,6 +15,7 @@ package java.nio.charset import scala.annotation.switch import java.nio._ +import java.util.JSUtils._ import scala.scalajs.js @@ -77,7 +78,7 @@ object CoderResult { } private def malformedForLengthImpl(length: Int): CoderResult = { - uniqueMalformed(length).fold { + undefOrFold(uniqueMalformed(length)) { val result = new CoderResult(Malformed, length) uniqueMalformed(length) = result result @@ -95,7 +96,7 @@ object CoderResult { } private def unmappableForLengthImpl(length: Int): CoderResult = { - uniqueUnmappable(length).fold { + undefOrFold(uniqueUnmappable(length)) { val result = new CoderResult(Unmappable, length) uniqueUnmappable(length) = result result diff --git a/javalib/src/main/scala/java/util/ArrayDeque.scala b/javalib/src/main/scala/java/util/ArrayDeque.scala index 9abd87a7b9..46ef2388a8 100644 --- a/javalib/src/main/scala/java/util/ArrayDeque.scala +++ b/javalib/src/main/scala/java/util/ArrayDeque.scala @@ -13,6 +13,7 @@ package java.util import java.lang.Cloneable +import java.util.JSUtils._ import scala.scalajs.js @@ -37,6 +38,9 @@ class ArrayDeque[E] private (private val inner: js.Array[E]) addAll(c) } + @inline + override def isEmpty(): Boolean = inner.length == 0 + def addFirst(e: E): Unit = offerFirst(e) @@ -64,21 +68,21 @@ class ArrayDeque[E] private (private val inner: js.Array[E]) } def removeFirst(): E = { - if (inner.isEmpty) + if (isEmpty()) throw new NoSuchElementException() else pollFirst() } def removeLast(): E = { - if (inner.isEmpty) + if (isEmpty()) throw new NoSuchElementException() else pollLast() } def pollFirst(): E = { - if (inner.isEmpty) null.asInstanceOf[E] + if (isEmpty()) null.asInstanceOf[E] else { val res = inner.shift() status += 1 @@ -87,52 +91,65 @@ class ArrayDeque[E] private (private val inner: js.Array[E]) } def pollLast(): E = { - if (inner.isEmpty) null.asInstanceOf[E] + if (isEmpty()) null.asInstanceOf[E] else inner.pop() } def getFirst(): E = { - if (inner.isEmpty) + if (isEmpty()) throw new NoSuchElementException() else peekFirst() } def getLast(): E = { - if (inner.isEmpty) + if (isEmpty()) throw new NoSuchElementException() else peekLast() } def peekFirst(): E = { - if (inner.isEmpty) null.asInstanceOf[E] - else inner.head + if (isEmpty()) null.asInstanceOf[E] + else inner(0) } def peekLast(): E = { - if (inner.isEmpty) null.asInstanceOf[E] - else inner.last + if (isEmpty()) null.asInstanceOf[E] + else inner(inner.length - 1) } def removeFirstOccurrence(o: Any): Boolean = { - val index = inner.indexWhere(Objects.equals(_, o)) - if (index >= 0) { - inner.remove(index) - status += 1 - true - } else - false + // scalastyle:off return + val inner = this.inner // local copy + val len = inner.length + var i = 0 + while (i != len) { + if (Objects.equals(inner(i), o)) { + arrayRemove(inner, i) + status += 1 + return true + } + i += 1 + } + false + // scalastyle:on return } def removeLastOccurrence(o: Any): Boolean = { - val index = inner.lastIndexWhere(Objects.equals(_, o)) - if (index >= 0) { - inner.remove(index) - status += 1 - true - } else - false + // scalastyle:off return + val inner = this.inner // local copy + var i = inner.length - 1 + while (i >= 0) { + if (Objects.equals(inner(i), o)) { + arrayRemove(inner, i) + status += 1 + return true + } + i -= 1 + } + false + // scalastyle:on return } override def add(e: E): Boolean = { @@ -154,7 +171,7 @@ class ArrayDeque[E] private (private val inner: js.Array[E]) def pop(): E = removeFirst() - def size(): Int = inner.size + def size(): Int = inner.length private def failFastIterator(startIndex: Int, nex: (Int) => Int) = { new Iterator[E] { @@ -169,7 +186,7 @@ class ArrayDeque[E] private (private val inner: js.Array[E]) def hasNext(): Boolean = { checkStatus() val n = nex(index) - (n >= 0) && (n < inner.size) + (n >= 0) && (n < inner.length) } def next(): E = { @@ -180,10 +197,10 @@ class ArrayDeque[E] private (private val inner: js.Array[E]) override def remove(): Unit = { checkStatus() - if (index < 0 || index >= inner.size) { + if (index < 0 || index >= inner.length) { throw new IllegalStateException() } else { - inner.remove(index) + arrayRemove(inner, index) } } } @@ -193,14 +210,16 @@ class ArrayDeque[E] private (private val inner: js.Array[E]) failFastIterator(-1, x => (x + 1)) def descendingIterator(): Iterator[E] = - failFastIterator(inner.size, x => (x - 1)) + failFastIterator(inner.length, x => (x - 1)) - override def contains(o: Any): Boolean = inner.exists(Objects.equals(_, o)) + override def contains(o: Any): Boolean = + arrayExists(inner)(Objects.equals(_, o)) override def remove(o: Any): Boolean = removeFirstOccurrence(o) override def clear(): Unit = { - if (!inner.isEmpty) status += 1 - inner.clear() + if (!isEmpty()) + status += 1 + inner.length = 0 } } diff --git a/javalib/src/main/scala/java/util/ArrayList.scala b/javalib/src/main/scala/java/util/ArrayList.scala index 3f573bc526..ad0e9b2d19 100644 --- a/javalib/src/main/scala/java/util/ArrayList.scala +++ b/javalib/src/main/scala/java/util/ArrayList.scala @@ -13,6 +13,7 @@ package java.util import java.lang.Cloneable +import java.util.JSUtils._ import scala.scalajs._ @@ -60,22 +61,22 @@ class ArrayList[E] private (private[ArrayList] val inner: js.Array[E]) } override def add(e: E): Boolean = { - inner += e + inner.push(e) true } override def add(index: Int, element: E): Unit = { checkIndexOnBounds(index) - inner.insert(index, element) + inner.splice(index, 0, element) } override def remove(index: Int): E = { checkIndexInBounds(index) - inner.remove(index) + arrayRemoveAndGet(inner, index) } override def clear(): Unit = - inner.clear() + inner.length = 0 override def addAll(index: Int, c: Collection[_ <: E]): Boolean = { c match { diff --git a/javalib/src/main/scala/java/util/Formatter.scala b/javalib/src/main/scala/java/util/Formatter.scala index 5535fda2cc..89d29e1923 100644 --- a/javalib/src/main/scala/java/util/Formatter.scala +++ b/javalib/src/main/scala/java/util/Formatter.scala @@ -18,6 +18,7 @@ import scala.scalajs.js import java.lang.{Double => JDouble} import java.io._ import java.math.{BigDecimal, BigInteger} +import java.util.JSUtils._ final class Formatter private (private[this] var dest: Appendable, formatterLocaleInfo: Formatter.LocaleInfo) @@ -83,7 +84,7 @@ final class Formatter private (private[this] var dest: Appendable, @noinline private def sendToDestSlowPath(ss: js.Array[String]): Unit = { trapIOExceptions { - ss.foreach(dest.append(_)) + forArrayElems(ss)(dest.append(_)) } } @@ -334,7 +335,7 @@ final class Formatter private (private[this] var dest: Appendable, * Int range. */ private def parsePositiveInt(capture: js.UndefOr[String]): Int = { - capture.fold { + undefOrFold(capture) { -1 } { s => val x = js.Dynamic.global.parseInt(s, 10).asInstanceOf[Double] diff --git a/javalib/src/main/scala/java/util/JSUtils.scala b/javalib/src/main/scala/java/util/JSUtils.scala new file mode 100644 index 0000000000..0f7d3ab22f --- /dev/null +++ b/javalib/src/main/scala/java/util/JSUtils.scala @@ -0,0 +1,182 @@ +/* + * 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 scala.language.implicitConversions + +import scala.scalajs.js +import scala.scalajs.js.annotation.JSBracketAccess + +private[java] object JSUtils { + @inline + def undefined: js.UndefOr[Nothing] = ().asInstanceOf[js.UndefOr[Nothing]] + + @inline + def isUndefined(x: Any): scala.Boolean = + x.asInstanceOf[AnyRef] eq ().asInstanceOf[AnyRef] + + @inline + def undefOrIsDefined[A](x: js.UndefOr[A]): scala.Boolean = + x ne ().asInstanceOf[AnyRef] + + @inline + def undefOrForceGet[A](x: js.UndefOr[A]): A = + x.asInstanceOf[A] + + @inline + def undefOrGetOrElse[A](x: js.UndefOr[A])(default: => A): A = + if (undefOrIsDefined(x)) x.asInstanceOf[A] + else default + + @inline + def undefOrGetOrNull[A >: Null](x: js.UndefOr[A]): A = + if (undefOrIsDefined(x)) x.asInstanceOf[A] + else null + + @inline + def undefOrForeach[A](x: js.UndefOr[A])(f: A => Any): Unit = { + if (undefOrIsDefined(x)) + f(undefOrForceGet(x)) + } + + @inline + def undefOrFold[A, B](x: js.UndefOr[A])(default: => B)(f: A => B): B = + if (undefOrIsDefined(x)) f(undefOrForceGet(x)) + else default + + private object Cache { + val safeHasOwnProperty = + js.Dynamic.global.Object.prototype.hasOwnProperty + .asInstanceOf[js.ThisFunction1[js.Dictionary[_], String, scala.Boolean]] + } + + @inline + private def safeHasOwnProperty(dict: js.Dictionary[_], key: String): scala.Boolean = + Cache.safeHasOwnProperty(dict, key) + + @js.native + private trait DictionaryRawApply[A] extends js.Object { + /** Reads a field of this object by its name. + * + * This must not be called if the dictionary does not contain the key. + */ + @JSBracketAccess + def rawApply(key: String): A = js.native + + /** Writes a field of this object. */ + @JSBracketAccess + def rawUpdate(key: String, value: A): Unit = js.native + } + + @inline + def dictEmpty[A](): js.Dictionary[A] = + new js.Object().asInstanceOf[js.Dictionary[A]] + + @inline + def dictGetOrElse[A](dict: js.Dictionary[_ <: A], key: String)( + default: => A): A = { + if (dictContains(dict, key)) + dictRawApply(dict, key) + else + default + } + + def dictGetOrElseAndRemove[A](dict: js.Dictionary[_ <: A], key: String, + default: A): A = { + if (dictContains(dict, key)) { + val result = dictRawApply(dict, key) + js.special.delete(dict, key) + result + } else { + default + } + } + + @inline + def dictRawApply[A](dict: js.Dictionary[A], key: String): A = + dict.asInstanceOf[DictionaryRawApply[A]].rawApply(key) + + def dictContains[A](dict: js.Dictionary[A], key: String): scala.Boolean = { + /* We have to use a safe version of hasOwnProperty, because + * "hasOwnProperty" could be a key of this dictionary. + */ + safeHasOwnProperty(dict, key) + } + + @inline + def dictSet[A](dict: js.Dictionary[A], key: String, value: A): Unit = + dict.asInstanceOf[DictionaryRawApply[A]].rawUpdate(key, value) + + @inline + def forArrayElems[A](array: js.Array[A])(f: A => Any): Unit = { + val len = array.length + var i = 0 + while (i != len) { + f(array(i)) + i += 1 + } + } + + @inline + def arrayRemove[A](array: js.Array[A], index: Int): Unit = + array.splice(index, 1) + + @inline + def arrayRemoveAndGet[A](array: js.Array[A], index: Int): A = + array.splice(index, 1)(0) + + @inline + def arrayExists[A](array: js.Array[A])(f: A => Boolean): Boolean = { + // scalastyle:off return + val len = array.length + var i = 0 + while (i != len) { + if (f(array(i))) + return true + i += 1 + } + false + // scalastyle:on return + } + + @js.native + private trait RawMap[K, V] extends js.Object { + def has(key: K): Boolean = js.native + def keys(): js.Iterator[K] = js.native + def set(key: K, value: V): js.Map[K, V] = js.native + def get(key: K): V = js.native + } + + @inline def mapHas[K, V](m: js.Map[K, V], key: K): Boolean = + m.asInstanceOf[RawMap[K, V]].has(key) + + @inline def mapGet[K, V](m: js.Map[K, V], key: K): V = + m.asInstanceOf[RawMap[K, V]].get(key) + + @inline def mapSet[K, V](m: js.Map[K, V], key: K, value: V): Unit = + m.asInstanceOf[RawMap[K, V]].set(key, value) + + @inline def mapGetOrElse[K, V](m: js.Map[K, V], key: K)(default: => V): V = + if (mapHas(m, key)) mapGet(m, key) + else default + + @inline def mapGetOrElseUpdate[K, V](m: js.Map[K, V], key: K)(default: => V): V = { + if (mapHas(m, key)) { + mapGet(m, key) + } else { + val value = default + mapSet(m, key, value) + value + } + } +} diff --git a/javalib/src/main/scala/java/util/TimerTask.scala b/javalib/src/main/scala/java/util/TimerTask.scala index 959d206f53..4299157e89 100644 --- a/javalib/src/main/scala/java/util/TimerTask.scala +++ b/javalib/src/main/scala/java/util/TimerTask.scala @@ -12,7 +12,8 @@ package java.util -import scala.scalajs.js.timers._ +import scala.scalajs.js +import scala.scalajs.js.timers.RawTimers._ import scala.scalajs.js.timers.SetTimeoutHandle abstract class TimerTask { @@ -42,7 +43,7 @@ abstract class TimerTask { private[util] def timeout(delay: Long)(body: => Unit): Unit = { if (!canceled) { - handle = setTimeout(delay.toDouble)(body) + handle = setTimeout(() => body, delay.toDouble) } } diff --git a/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala b/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala index f4fd855cec..16d35937fd 100644 --- a/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala +++ b/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala @@ -16,6 +16,7 @@ import java.lang.Cloneable import java.lang.{reflect => jlr} import java.util._ import java.util.function.{Predicate, UnaryOperator} +import java.util.JSUtils._ import scala.annotation.tailrec @@ -47,7 +48,7 @@ class CopyOnWriteArrayList[E <: AnyRef] private (private var inner: js.Array[E]) } def size(): Int = - inner.size + inner.length def isEmpty(): Boolean = size() == 0 @@ -291,7 +292,7 @@ class CopyOnWriteArrayList[E <: AnyRef] private (private var inner: js.Array[E]) } protected def innerRemove(index: Int): E = - inner.splice(index, 1)(0) + arrayRemoveAndGet(inner, index) protected def innerRemoveMany(index: Int, count: Int): Unit = inner.splice(index, count) diff --git a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala index 4b866920b0..257e399807 100644 --- a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala +++ b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala @@ -14,6 +14,8 @@ package java.util.regex import scala.annotation.{tailrec, switch} +import java.util.JSUtils._ + import scala.scalajs.js import Pattern.IndicesArray @@ -79,7 +81,7 @@ private[regex] class IndicesBuilder private (pattern: String, flags: String, } val start = index // by definition - val end = start + allMatchResult(0).get.length() + val end = start + undefOrForceGet(allMatchResult(0)).length() /* Initialize the `indices` array with: * - `[start, end]` at index 0, which represents the whole match, and @@ -91,10 +93,10 @@ private[regex] class IndicesBuilder private (pattern: String, flags: String, */ val len = groupCount + 1 val indices = new IndicesArray(len) - indices(0) = js.Tuple2(start, end) + indices(0) = js.Array(start, end).asInstanceOf[js.Tuple2[Int, Int]] var i = 1 while (i != len) { - indices(i) = js.undefined + indices(i) = undefined i += 1 } @@ -179,7 +181,7 @@ private[regex] object IndicesBuilder { final def propagateFromEnd(matchResult: js.RegExp.ExecResult, indices: IndicesArray, end: Int): Unit = { - val start = matchResult(newGroup).fold(-1)(matched => end - matched.length) + val start = undefOrFold(matchResult(newGroup))(-1)(matched => end - matched.length) propagate(matchResult, indices, start, end) } @@ -191,7 +193,7 @@ private[regex] object IndicesBuilder { final def propagateFromStart(matchResult: js.RegExp.ExecResult, indices: IndicesArray, start: Int): Int = { - val end = matchResult(newGroup).fold(-1)(matched => start + matched.length) + val end = undefOrFold(matchResult(newGroup))(-1)(matched => start + matched.length) propagate(matchResult, indices, start, end) end } @@ -212,8 +214,8 @@ private[regex] object IndicesBuilder { * always keep the default `-1` if this group node does not match * anything. */ - if (matchResult(newGroup).isDefined) - indices(number) = js.Tuple2(start, end) + if (undefOrIsDefined(matchResult(newGroup))) + indices(number) = js.Array(start, end).asInstanceOf[js.Tuple2[Int, Int]] inner.propagate(matchResult, indices, start, end) } } diff --git a/javalib/src/main/scala/java/util/regex/Matcher.scala b/javalib/src/main/scala/java/util/regex/Matcher.scala index 4effe7de81..6385dbd96a 100644 --- a/javalib/src/main/scala/java/util/regex/Matcher.scala +++ b/javalib/src/main/scala/java/util/regex/Matcher.scala @@ -12,6 +12,8 @@ package java.util.regex +import java.util.JSUtils._ + import scala.annotation.switch import scala.scalajs.js @@ -182,13 +184,13 @@ final class Matcher private[regex] ( def start(): Int = ensureLastMatch.index + regionStart() def end(): Int = start() + group().length - def group(): String = ensureLastMatch(0).get + def group(): String = undefOrForceGet(ensureLastMatch(0)) private def indices: IndicesArray = pattern().getIndices(ensureLastMatch, lastMatchIsForMatches) private def startInternal(compiledGroup: Int): Int = - indices(compiledGroup).fold(-1)(_._1 + regionStart()) + undefOrFold(indices(compiledGroup))(-1)(_._1 + regionStart()) def start(group: Int): Int = startInternal(pattern().numberedGroup(group)) @@ -197,7 +199,7 @@ final class Matcher private[regex] ( startInternal(pattern().namedGroup(name)) private def endInternal(compiledGroup: Int): Int = - indices(compiledGroup).fold(-1)(_._2 + regionStart()) + undefOrFold(indices(compiledGroup))(-1)(_._2 + regionStart()) def end(group: Int): Int = endInternal(pattern().numberedGroup(group)) @@ -206,10 +208,10 @@ final class Matcher private[regex] ( endInternal(pattern().namedGroup(name)) def group(group: Int): String = - ensureLastMatch(pattern().numberedGroup(group)).orNull + undefOrGetOrNull(ensureLastMatch(pattern().numberedGroup(group))) def group(name: String): String = - ensureLastMatch(pattern().namedGroup(name)).orNull + undefOrGetOrNull(ensureLastMatch(pattern().namedGroup(name))) // Seal the state @@ -266,7 +268,7 @@ object Matcher { def start(): Int = ensureLastMatch.index + regionStart def end(): Int = start() + group().length - def group(): String = ensureLastMatch(0).get + def group(): String = undefOrForceGet(ensureLastMatch(0)) private def indices: IndicesArray = pattern.getIndices(ensureLastMatch, lastMatchIsForMatches) @@ -276,13 +278,13 @@ object Matcher { */ def start(group: Int): Int = - indices(pattern.numberedGroup(group)).fold(-1)(_._1 + regionStart) + undefOrFold(indices(pattern.numberedGroup(group)))(-1)(_._1 + regionStart) def end(group: Int): Int = - indices(pattern.numberedGroup(group)).fold(-1)(_._2 + regionStart) + undefOrFold(indices(pattern.numberedGroup(group)))(-1)(_._2 + regionStart) def group(group: Int): String = - ensureLastMatch(pattern.numberedGroup(group)).orNull + undefOrGetOrNull(ensureLastMatch(pattern.numberedGroup(group))) private def ensureLastMatch: js.RegExp.ExecResult = { if (lastMatch == null) diff --git a/javalib/src/main/scala/java/util/regex/Pattern.scala b/javalib/src/main/scala/java/util/regex/Pattern.scala index fc747f0eba..a26bff33d0 100644 --- a/javalib/src/main/scala/java/util/regex/Pattern.scala +++ b/javalib/src/main/scala/java/util/regex/Pattern.scala @@ -14,6 +14,7 @@ package java.util.regex import scala.annotation.tailrec +import java.util.JSUtils._ import java.util.ScalaOps._ import scala.scalajs.js @@ -132,14 +133,14 @@ final class Pattern private[regex] ( } private[regex] def namedGroup(name: String): Int = { - groupNumberMap(namedGroups.getOrElse(name, { + groupNumberMap(dictGetOrElse(namedGroups, name) { throw new IllegalArgumentException(s"No group with name <$name>") - })) + }) } private[regex] def getIndices(lastMatch: js.RegExp.ExecResult, forMatches: Boolean): IndicesArray = { val lastMatchDyn = lastMatch.asInstanceOf[js.Dynamic] - if (js.isUndefined(lastMatchDyn.indices)) { + if (isUndefined(lastMatchDyn.indices)) { if (supportsIndices) { if (!enabledNativeIndices) { jsRegExpForFind = new js.RegExp(jsPattern, jsFlagsForFind + "d") diff --git a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala index bdc9593238..5011bab65a 100644 --- a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala +++ b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala @@ -25,10 +25,12 @@ import java.lang.Character.{ MAX_LOW_SURROGATE } +import java.util.JSUtils._ import java.util.ScalaOps._ import scala.scalajs.js -import scala.scalajs.LinkingInfo.{ESVersion, esVersion} +import scala.scalajs.runtime.linkingInfo +import scala.scalajs.LinkingInfo.ESVersion /** Compiler from Java regular expressions to JavaScript regular expressions. * @@ -80,15 +82,15 @@ private[regex] object PatternCompiler { /** Cache for `Support.supportsUnicode`. */ private val _supportsUnicode = - (esVersion >= ESVersion.ES2015) || featureTest("u") + (linkingInfo.esVersion >= ESVersion.ES2015) || featureTest("u") /** Cache for `Support.supportsSticky`. */ private val _supportsSticky = - (esVersion >= ESVersion.ES2015) || featureTest("y") + (linkingInfo.esVersion >= ESVersion.ES2015) || featureTest("y") /** Cache for `Support.supportsDotAll`. */ private val _supportsDotAll = - (esVersion >= ESVersion.ES2018) || featureTest("us") + (linkingInfo.esVersion >= ESVersion.ES2018) || featureTest("us") /** Cache for `Support.supportsIndices`. */ private val _supportsIndices = @@ -104,17 +106,17 @@ private[regex] object PatternCompiler { /** Tests whether the underlying JS RegExp supports the 'u' flag. */ @inline def supportsUnicode: Boolean = - (esVersion >= ESVersion.ES2015) || _supportsUnicode + (linkingInfo.esVersion >= ESVersion.ES2015) || _supportsUnicode /** Tests whether the underlying JS RegExp supports the 'y' flag. */ @inline def supportsSticky: Boolean = - (esVersion >= ESVersion.ES2015) || _supportsSticky + (linkingInfo.esVersion >= ESVersion.ES2015) || _supportsSticky /** Tests whether the underlying JS RegExp supports the 's' flag. */ @inline def supportsDotAll: Boolean = - (esVersion >= ESVersion.ES2018) || _supportsDotAll + (linkingInfo.esVersion >= ESVersion.ES2018) || _supportsDotAll /** Tests whether the underlying JS RegExp supports the 'd' flag. */ @inline @@ -128,7 +130,7 @@ private[regex] object PatternCompiler { */ @inline def enableUnicodeCaseInsensitive: Boolean = - esVersion >= ESVersion.ES2015 + linkingInfo.esVersion >= ESVersion.ES2015 /** Tests whether features requiring \p{} and/or look-behind assertions are enabled. * @@ -137,7 +139,7 @@ private[regex] object PatternCompiler { */ @inline def enableUnicodeCharacterClassesAndLookBehinds: Boolean = - esVersion >= ESVersion.ES2018 + linkingInfo.esVersion >= ESVersion.ES2018 } import Support._ @@ -212,7 +214,7 @@ private[regex] object PatternCompiler { import InlinedHelpers._ private def codePointToString(codePoint: Int): String = { - if (esVersion >= ESVersion.ES2015) { + if (linkingInfo.esVersion >= ESVersion.ES2015) { js.Dynamic.global.String.fromCodePoint(codePoint).asInstanceOf[String] } else { if (isBmpCodePoint(codePoint)) { @@ -286,24 +288,24 @@ private[regex] object PatternCompiler { * This is a `js.Dictionary` because it can be used even when compiling to * ECMAScript 5.1. */ - private val asciiPOSIXCharacterClasses = { + private val asciiPOSIXCharacterClasses: js.Dictionary[CompiledCharClass] = { import CompiledCharClass._ - js.Dictionary( - ("Lower", posClass("a-z")), - ("Upper", posClass("A-Z")), - ("ASCII", posClass("\u0000-\u007f")), - ("Alpha", posClass("A-Za-z")), // [\p{Lower}\p{Upper}] - ("Digit", posClass("0-9")), - ("Alnum", posClass("0-9A-Za-z")), // [\p{Alpha}\p{Digit}] - ("Punct", posClass("!-/:-@[-`{-~")), // One of !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ - ("Graph", posClass("!-~")), // [\p{Alnum}\p{Punct}] - ("Print", posClass(" -~")), // [\p{Graph}\x20] - ("Blank", posClass("\t ")), - ("Cntrl", posClass("\u0000-\u001f\u007f")), - ("XDigit", posClass("0-9A-Fa-f")), - ("Space", posClass("\t-\r ")) // [ \t\n\x0B\f\r] - ) + val r = dictEmpty[CompiledCharClass]() + dictSet(r, "Lower", posClass("a-z")) + dictSet(r, "Upper", posClass("A-Z")) + dictSet(r, "ASCII", posClass("\u0000-\u007f")) + dictSet(r, "Alpha", posClass("A-Za-z")) // [\p{Lower}\p{Upper}] + dictSet(r, "Digit", posClass("0-9")) + dictSet(r, "Alnum", posClass("0-9A-Za-z")) // [\p{Alpha}\p{Digit}] + dictSet(r, "Punct", posClass("!-/:-@[-`{-~")) // One of !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ + dictSet(r, "Graph", posClass("!-~")) // [\p{Alnum}\p{Punct}] + dictSet(r, "Print", posClass(" -~")) // [\p{Graph}\x20] + dictSet(r, "Blank", posClass("\t ")) + dictSet(r, "Cntrl", posClass("\u0000-\u001f\u007f")) + dictSet(r, "XDigit", posClass("0-9A-Fa-f")) + dictSet(r, "Space", posClass("\t-\r ")) // [ \t\n\x0B\f\r] + r } /** Mapping of predefined character classes to the corresponding character @@ -333,70 +335,70 @@ private[regex] object PatternCompiler { "Cc", "Cf", "Cs", "Co", "Cn", "C" ) - for (gc <- generalCategories) { + forArrayElems(generalCategories) { gc => val compiled = posP(gc) - result(gc) = compiled - result("Is" + gc) = compiled - result("general_category=" + gc) = compiled - result("gc=" + gc) = compiled + mapSet(result, gc, compiled) + mapSet(result, "Is" + gc, compiled) + mapSet(result, "general_category=" + gc, compiled) + mapSet(result, "gc=" + gc, compiled) } // Binary properties - result("IsAlphabetic") = posP("Alphabetic") - result("IsIdeographic") = posP("Ideographic") - result("IsLetter") = posP("Letter") - result("IsLowercase") = posP("Lowercase") - result("IsUppercase") = posP("Uppercase") - result("IsTitlecase") = posP("Lt") - result("IsPunctuation") = posP("Punctuation") - result("IsControl") = posP("Control") - result("IsWhite_Space") = posP("White_Space") - result("IsDigit") = posP("Nd") - result("IsHex_Digit") = posP("Hex_Digit") - result("IsJoin_Control") = posP("Join_Control") - result("IsNoncharacter_Code_Point") = posP("Noncharacter_Code_Point") - result("IsAssigned") = posP("Assigned") + mapSet(result, "IsAlphabetic", posP("Alphabetic")) + mapSet(result, "IsIdeographic", posP("Ideographic")) + mapSet(result, "IsLetter", posP("Letter")) + mapSet(result, "IsLowercase", posP("Lowercase")) + mapSet(result, "IsUppercase", posP("Uppercase")) + mapSet(result, "IsTitlecase", posP("Lt")) + mapSet(result, "IsPunctuation", posP("Punctuation")) + mapSet(result, "IsControl", posP("Control")) + mapSet(result, "IsWhite_Space", posP("White_Space")) + mapSet(result, "IsDigit", posP("Nd")) + mapSet(result, "IsHex_Digit", posP("Hex_Digit")) + mapSet(result, "IsJoin_Control", posP("Join_Control")) + mapSet(result, "IsNoncharacter_Code_Point", posP("Noncharacter_Code_Point")) + mapSet(result, "IsAssigned", posP("Assigned")) // java.lang.Character classes - result("javaAlphabetic") = posP("Alphabetic") - result("javaDefined") = negP("Cn") - result("javaDigit") = posP("Nd") - result("javaIdentifierIgnorable") = posClass("\u0000-\u0008\u000E-\u001B\u007F-\u009F\\p{Cf}") - result("javaIdeographic") = posP("Ideographic") - result("javaISOControl") = posClass("\u0000-\u001F\u007F-\u009F") - result("javaJavaIdentifierPart") = - posClass("\\p{L}\\p{Sc}\\p{Pc}\\p{Nd}\\p{Nl}\\p{Mn}\\p{Mc}\u0000-\u0008\u000E-\u001B\u007F-\u009F\\p{Cf}") - result("javaJavaIdentifierStart") = posClass("\\p{L}\\p{Sc}\\p{Pc}\\p{Nl}") - result("javaLetterOrDigit") = posClass("\\p{L}\\p{Nd}") - result("javaLowerCase") = posP("Lowercase") - result("javaMirrored") = posP("Bidi_Mirrored") - result("javaSpaceChar") = posP("Z") - result("javaTitleCase") = posP("Lt") - result("javaUnicodeIdentifierPart") = - posClass("\\p{ID_Continue}\u2E2F\u0000-\u0008\u000E-\u001B\u007F-\u009F\\p{Cf}") - result("javaUnicodeIdentifierStart") = posClass("\\p{ID_Start}\u2E2F") - result("javaUpperCase") = posP("Uppercase") + mapSet(result, "javaAlphabetic", posP("Alphabetic")) + mapSet(result, "javaDefined", negP("Cn")) + mapSet(result, "javaDigit", posP("Nd")) + mapSet(result, "javaIdentifierIgnorable", posClass("\u0000-\u0008\u000E-\u001B\u007F-\u009F\\p{Cf}")) + mapSet(result, "javaIdeographic", posP("Ideographic")) + mapSet(result, "javaISOControl", posClass("\u0000-\u001F\u007F-\u009F")) + mapSet(result, "javaJavaIdentifierPart", + posClass("\\p{L}\\p{Sc}\\p{Pc}\\p{Nd}\\p{Nl}\\p{Mn}\\p{Mc}\u0000-\u0008\u000E-\u001B\u007F-\u009F\\p{Cf}")) + mapSet(result, "javaJavaIdentifierStart", posClass("\\p{L}\\p{Sc}\\p{Pc}\\p{Nl}")) + mapSet(result, "javaLetterOrDigit", posClass("\\p{L}\\p{Nd}")) + mapSet(result, "javaLowerCase", posP("Lowercase")) + mapSet(result, "javaMirrored", posP("Bidi_Mirrored")) + mapSet(result, "javaSpaceChar", posP("Z")) + mapSet(result, "javaTitleCase", posP("Lt")) + mapSet(result, "javaUnicodeIdentifierPart", + posClass("\\p{ID_Continue}\u2E2F\u0000-\u0008\u000E-\u001B\u007F-\u009F\\p{Cf}")) + mapSet(result, "javaUnicodeIdentifierStart", posClass("\\p{ID_Start}\u2E2F")) + mapSet(result, "javaUpperCase", posP("Uppercase")) // [\t-\r\u001C-\u001F\\p{Z}&&[^\u00A0\u2007\u202F]] - result("javaWhitespace") = - posClass("\t-\r\u001C-\u001F \u1680\u2000-\u2006\u2008-\u200A\u205F\u3000\\p{Zl}\\p{Zp}") + mapSet(result, "javaWhitespace", + posClass("\t-\r\u001C-\u001F \u1680\u2000-\u2006\u2008-\u200A\u205F\u3000\\p{Zl}\\p{Zp}")) /* POSIX character classes with Unicode compatibility * (resolved from the original definitions, which are in comments) */ - result("Lower") = posP("Lower") // \p{IsLowercase} - result("Upper") = posP("Upper") // \p{IsUppercase} - result("ASCII") = posClass("\u0000-\u007f") - result("Alpha") = posP("Alpha") // \p{IsAlphabetic} - result("Digit") = posP("Nd") // \p{IsDigit} - result("Alnum") = posClass("\\p{Alpha}\\p{Nd}") // [\p{IsAlphabetic}\p{IsDigit}] - result("Punct") = posP("P") // \p{IsPunctuation} + mapSet(result, "Lower", posP("Lower")) // \p{IsLowercase} + mapSet(result, "Upper", posP("Upper")) // \p{IsUppercase} + mapSet(result, "ASCII", posClass("\u0000-\u007f")) + mapSet(result, "Alpha", posP("Alpha")) // \p{IsAlphabetic} + mapSet(result, "Digit", posP("Nd")) // \p{IsDigit} + mapSet(result, "Alnum", posClass("\\p{Alpha}\\p{Nd}")) // [\p{IsAlphabetic}\p{IsDigit}] + mapSet(result, "Punct", posP("P")) // \p{IsPunctuation} // [^\p{IsWhite_Space}\p{gc=Cc}\p{gc=Cs}\p{gc=Cn}] - result("Graph") = negClass("\\p{White_Space}\\p{Cc}\\p{Cs}\\p{Cn}") + mapSet(result, "Graph", negClass("\\p{White_Space}\\p{Cc}\\p{Cs}\\p{Cn}")) /* [\p{Graph}\p{Blank}&&[^\p{Cntrl}]] * === (by definition of Cntrl) @@ -416,7 +418,7 @@ private[regex] object PatternCompiler { * === (because \x09-\x0d and \x85 are all in the Cc category) * [^\p{Zl}\p{Zp}\p{Cc}\p{Cs}\p{Cn}] */ - result("Print") = negClass("\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cs}\\p{Cn}") + mapSet(result, "Print", negClass("\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cs}\\p{Cn}")) /* [\p{IsWhite_Space}&&[^\p{gc=Zl}\p{gc=Zp}\x0a\x0b\x0c\x0d\x85]] * === (see the excerpt from PropList.txt below) @@ -424,11 +426,11 @@ private[regex] object PatternCompiler { * === (by simplification) * [\x09\p{gc=Zs}] */ - result("Blank") = posClass("\t\\p{Zs}") + mapSet(result, "Blank", posClass("\t\\p{Zs}")) - result("Cntrl") = posP("Cc") // \p{gc=Cc} - result("XDigit") = posClass("\\p{Nd}\\p{Hex}") // [\p{gc=Nd}\p{IsHex_Digit}] - result("Space") = posP("White_Space") // \p{IsWhite_Space} + mapSet(result, "Cntrl", posP("Cc")) // \p{gc=Cc} + mapSet(result, "XDigit", posClass("\\p{Nd}\\p{Hex}")) // [\p{gc=Nd}\p{IsHex_Digit}] + mapSet(result, "Space", posP("White_Space")) // \p{IsWhite_Space} result } @@ -473,7 +475,7 @@ private[regex] object PatternCompiler { /* SignWriting is an exception. It has an uppercase 'W' even though it is * not after '_'. We add the exception to the map immediately. */ - result("signwriting") = "SignWriting" + mapSet(result, "signwriting", "SignWriting") result } @@ -741,7 +743,7 @@ private final class PatternCompiler(private val pattern: String, private var fla * We store *original* group numbers, rather than compiled group numbers, * in order to make the renumbering caused by possessive quantifiers easier. */ - private val namedGroups = js.Dictionary.empty[Int] + private val namedGroups = dictEmpty[Int]() @inline private def hasFlag(flag: Int): Boolean = (flags & flag) != 0 @@ -850,7 +852,7 @@ private final class PatternCompiler(private val pattern: String, private var fla private def processLeadingEmbeddedFlags(): Unit = { val m = leadingEmbeddedFlagSpecifierRegExp.exec(pattern) if (m != null) { - for (chars <- m(1)) { + undefOrForeach(m(1)) { chars => for (i <- 0 until chars.length()) flags |= charToFlag(chars.charAt(i)) } @@ -859,7 +861,7 @@ private final class PatternCompiler(private val pattern: String, private var fla if (hasFlag(UNICODE_CHARACTER_CLASS)) flags |= UNICODE_CASE - for (chars <- m(2)) { + undefOrForeach(m(2)) { chars => for (i <- 0 until chars.length()) flags &= ~charToFlag(chars.charAt(i)) } @@ -872,7 +874,7 @@ private final class PatternCompiler(private val pattern: String, private var fla */ // Advance past the embedded flags - pIndex += m(0).get.length() + pIndex += undefOrForceGet(m(0)).length() } } @@ -1362,9 +1364,9 @@ 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 = namedGroups.getOrElse(groupName, { + val groupNumber = dictGetOrElse(namedGroups, groupName) { parseError(s"named capturing group <$groupName> does not exit") - }) + } val compiledGroupNumber = groupNumberMap(groupNumber) pIndex += 1 // Wrap in a non-capturing group in case it's followed by a (de-escaped) digit @@ -1607,16 +1609,16 @@ private final class PatternCompiler(private val pattern: String, private var fla pattern.substring(start, start + 1) } - val result = if (!unicodeCharacterClass && asciiPOSIXCharacterClasses.contains(property)) { + val result = if (!unicodeCharacterClass && dictContains(asciiPOSIXCharacterClasses, property)) { val property2 = if (asciiCaseInsensitive && (property == "Lower" || property == "Upper")) "Alpha" else property - asciiPOSIXCharacterClasses(property2) + dictRawApply(asciiPOSIXCharacterClasses, property2) } else { // For anything else, we need built-in support for \p requireES2018Features("Unicode character family") - predefinedPCharacterClasses.getOrElse(property, { + mapGetOrElse(predefinedPCharacterClasses, property) { val scriptPrefixLen = if (property.startsWith("Is")) { 2 } else if (property.startsWith("sc=")) { @@ -1630,7 +1632,7 @@ private final class PatternCompiler(private val pattern: String, private var fla parseError(s"Unknown Unicode character class '$property'") } CompiledCharClass.posP("sc=" + canonicalizeScriptName(property.substring(scriptPrefixLen))) - }) + } } pIndex += 1 @@ -1651,7 +1653,7 @@ private final class PatternCompiler(private val pattern: String, private var fla val lowercase = scriptName.toLowerCase() - canonicalizedScriptNameCache.getOrElseUpdate(lowercase, { + mapGetOrElseUpdate(canonicalizedScriptNameCache, lowercase) { val canonical = lowercase.jsReplace(scriptCanonicalizeRegExp, ((s: String) => s.toUpperCase()): js.Function1[String, String]) @@ -1663,7 +1665,7 @@ private final class PatternCompiler(private val pattern: String, private var fla } canonical - }) + } } private def compileCharacterClass(): String = { @@ -1805,11 +1807,11 @@ private final class PatternCompiler(private val pattern: String, private var fla // Named capturing group pIndex = start + 3 val name = parseGroupName() - if (namedGroups.contains(name)) + if (dictContains(namedGroups, name)) parseError(s"named capturing group <$name> is already defined") compiledGroupCount += 1 groupNumberMap.push(compiledGroupCount) // this changes originalGroupCount - namedGroups(name) = originalGroupCount + dictSet(namedGroups, name, originalGroupCount) pIndex += 1 "(" + compileInsideGroup() + ")" } else { diff --git a/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala b/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala index 0faf78fafc..99937b1550 100644 --- a/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala +++ b/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala @@ -13,6 +13,7 @@ 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) @@ -41,7 +42,7 @@ class PatternSyntaxException(desc: String, regex: String, index: Int) @inline private def repeat(s: String, count: Int): String = { // TODO Use java.lang.String.repeat() once we can (JDK 11+ method) - if (LinkingInfo.esVersion >= LinkingInfo.ESVersion.ES2015) { + if (linkingInfo.esVersion >= LinkingInfo.ESVersion.ES2015) { s.asInstanceOf[js.Dynamic].repeat(count).asInstanceOf[String] } else { var result = "" diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 8e955e5b8d..b7ead14157 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 146336, - expectedFullLinkSizeWithoutClosure = 135798, - expectedFullLinkSizeWithClosure = 22074, + expectedFastLinkSize = 145128, + expectedFullLinkSizeWithoutClosure = 134589, + expectedFullLinkSizeWithClosure = 21831, classDefs, moduleInitializers = MainTestModuleInitializers ) From d917d1d75479dedd2c7f54fbcc29a946d98f4993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 10 Jun 2022 13:28:17 +0200 Subject: [PATCH 28/75] Work around a Scala 2.11 limitation with the IR cleaner. We cannot have nested anonymous functions, otherwise the outer one is not optimized and is a full class extending AbstractFunctionN. --- .../src/main/scala/java/math/Division.scala | 5 +- .../main/scala/java/math/Multiplication.scala | 10 +++- javalib/src/main/scala/java/util/BitSet.scala | 18 +++++--- .../src/main/scala/java/util/Formatter.scala | 6 ++- javalib/src/main/scala/java/util/Timer.scala | 46 ++++++++++--------- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/javalib/src/main/scala/java/math/Division.scala b/javalib/src/main/scala/java/math/Division.scala index f895fc5fe1..a69382135b 100644 --- a/javalib/src/main/scala/java/math/Division.scala +++ b/javalib/src/main/scala/java/math/Division.scala @@ -884,11 +884,14 @@ private[math] object Division { for (i <- 0 until modulusLen) { var innnerCarry: Int = 0 // unsigned val m = Multiplication.unsignedMultAddAdd(res(i), n2, 0, 0).toInt - for (j <- 0 until modulusLen) { + // Work around Scala 2.11 limitation with the IR cleaner ; should be for (j <- 0 until modulusLen) + var j = 0 + while (j < modulusLen) { val nextInnnerCarry = unsignedMultAddAdd(m, modulusDigits(j), res(i + j), innnerCarry) res(i + j) = nextInnnerCarry.toInt innnerCarry = (nextInnnerCarry >> 32).toInt + j += 1 } val nextOuterCarry = (outerCarry & UINT_MAX) + (res(i + modulusLen) & UINT_MAX) + (innnerCarry & UINT_MAX) diff --git a/javalib/src/main/scala/java/math/Multiplication.scala b/javalib/src/main/scala/java/math/Multiplication.scala index 859d9f926f..10ecb738cc 100644 --- a/javalib/src/main/scala/java/math/Multiplication.scala +++ b/javalib/src/main/scala/java/math/Multiplication.scala @@ -124,10 +124,13 @@ private[math] object Multiplication { for (i <- 0 until aLen) { carry = 0 - for (j <- i + 1 until aLen) { + // Work around Scala 2.11 limitation with the IR cleaner ; should be for (j <- i + 1 until aLen) + var j = i + 1 + while (j < aLen) { val t = unsignedMultAddAdd(a(i), a(j), res(i + j), carry) res(i + j) = t.toInt carry = (t >>> 32).toInt + j += 1 } res(i + aLen) = carry } @@ -439,10 +442,13 @@ private[math] object Multiplication { for (i <- 0 until aLen) { var carry = 0 val aI = a(i) - for (j <- 0 until bLen) { + // Work around Scala 2.11 limitation with the IR cleaner ; should be for (j <- 0 until bLen) + var j = 0 + while (j < bLen) { val added = unsignedMultAddAdd(aI, b(j), t(i + j), carry) t(i + j) = added.toInt carry = (added >>> 32).toInt + j += 1 } t(i + bLen) = carry } diff --git a/javalib/src/main/scala/java/util/BitSet.scala b/javalib/src/main/scala/java/util/BitSet.scala index 5e2c4bd61f..171ed1a629 100644 --- a/javalib/src/main/scala/java/util/BitSet.scala +++ b/javalib/src/main/scala/java/util/BitSet.scala @@ -637,16 +637,20 @@ class BitSet private (private var bits: Array[Int]) extends Serializable with Cl var result: String = "{" var comma: Boolean = false + // Work around Scala 2.11 limitation with the IR cleaner ; should be double-for over i and j for { i <- 0 until getActualArrayLength() - j <- 0 until ElementSize } { - if ((bits(i) & (1 << j)) != 0) { - if (comma) - result += ", " - else - comma = true - result += (i << AddressBitsPerWord) + j + var j = 0 + while (j < ElementSize) { + if ((bits(i) & (1 << j)) != 0) { + if (comma) + result += ", " + else + comma = true + result += (i << AddressBitsPerWord) + j + } + j += 1 } } diff --git a/javalib/src/main/scala/java/util/Formatter.scala b/javalib/src/main/scala/java/util/Formatter.scala index 89d29e1923..5807a2ddcf 100644 --- a/javalib/src/main/scala/java/util/Formatter.scala +++ b/javalib/src/main/scala/java/util/Formatter.scala @@ -83,8 +83,12 @@ final class Formatter private (private[this] var dest: Appendable, @noinline private def sendToDestSlowPath(ss: js.Array[String]): Unit = { - trapIOExceptions { + // Workaround Scala 2.11 limitation: cannot nest anonymous functions for the IR cleaner + @inline def body(): Unit = forArrayElems(ss)(dest.append(_)) + + trapIOExceptions { + body() } } diff --git a/javalib/src/main/scala/java/util/Timer.scala b/javalib/src/main/scala/java/util/Timer.scala index ac75a1e61d..4be9d67d43 100644 --- a/javalib/src/main/scala/java/util/Timer.scala +++ b/javalib/src/main/scala/java/util/Timer.scala @@ -70,16 +70,18 @@ class Timer() { private def schedulePeriodically( task: TimerTask, delay: Long, period: Long): Unit = { acquire(task) - task.timeout(delay) { - def loop(): Unit = { - val startTime = System.nanoTime() - task.doRun() - val endTime = System.nanoTime() - val duration = (endTime - startTime) / 1000000 - task.timeout(period - duration) { - loop() - } + + def loop(): Unit = { + val startTime = System.nanoTime() + task.doRun() + val endTime = System.nanoTime() + val duration = (endTime - startTime) / 1000000 + task.timeout(period - duration) { + loop() } + } + + task.timeout(delay) { loop() } } @@ -100,21 +102,23 @@ class Timer() { private def scheduleFixed( task: TimerTask, delay: Long, period: Long): Unit = { acquire(task) - task.timeout(delay) { - def loop(scheduledTime: Long): Unit = { - task.doRun() - val nextScheduledTime = scheduledTime + period - val nowTime = System.nanoTime / 1000000L - if (nowTime >= nextScheduledTime) { - // Re-run immediately. + + def loop(scheduledTime: Long): Unit = { + task.doRun() + val nextScheduledTime = scheduledTime + period + val nowTime = System.nanoTime / 1000000L + if (nowTime >= nextScheduledTime) { + // Re-run immediately. + loop(nextScheduledTime) + } else { + // Re-run after a timeout. + task.timeout(nextScheduledTime - nowTime) { loop(nextScheduledTime) - } else { - // Re-run after a timeout. - task.timeout(nextScheduledTime - nowTime) { - loop(nextScheduledTime) - } } } + } + + task.timeout(delay) { loop(System.nanoTime / 1000000L + period) } } From 2ad751df7772a1c9ad009b3741c97c76bec8da27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 2 Jul 2022 14:19:01 +0200 Subject: [PATCH 29/75] Work around a Scala 2.11 compiler crash with -no-specialization. --- javalib/src/main/scala/java/io/BufferedReader.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/javalib/src/main/scala/java/io/BufferedReader.scala b/javalib/src/main/scala/java/io/BufferedReader.scala index 3257c5b9d1..4cb0afdd32 100644 --- a/javalib/src/main/scala/java/io/BufferedReader.scala +++ b/javalib/src/main/scala/java/io/BufferedReader.scala @@ -16,7 +16,9 @@ class BufferedReader(in: Reader, sz: Int) extends Reader { def this(in: Reader) = this(in, 4096) - private[this] var buf = new Array[Char](sz) + // Workaround 2.11 with no-specialization ; buf should be initialized on the same line + private[this] var buf: Array[Char] = null + buf = new Array[Char](sz) /** Last valid value in the buffer (exclusive) */ private[this] var end = 0 From 93d3997b08720b7cb108acbc23677b6949dc03ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 9 Aug 2019 13:36:27 +0200 Subject: [PATCH 30/75] Enable the IR cleaner on the javalib. --- .../src/test/scala/org/scalajs/linker/LibrarySizeTest.scala | 6 +++--- project/Build.scala | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index b7ead14157..571e1f0656 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 145128, - expectedFullLinkSizeWithoutClosure = 134589, - expectedFullLinkSizeWithClosure = 21831, + expectedFastLinkSize = 144675, + expectedFullLinkSizeWithoutClosure = 133818, + expectedFullLinkSizeWithClosure = 21620, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index 552563ebde..2a61950768 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1271,6 +1271,8 @@ object Build { Nil }, + cleanIRSettings, + headerSources in Compile ~= { srcs => srcs.filter { src => val path = src.getPath.replace('\\', '/') From 2103212164c4ba717940991ec3bf222b17073d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 25 Jul 2022 18:34:07 +0200 Subject: [PATCH 31/75] Manually inline System.identityHashCode into Object.hashCode. Since there is a dedicated IR node for the identity hash code, we can directly use that in `j.l.Object.hashCode()`. This simplifies the hard-coded IR of `j.l.Object`, and allows to remove `System` from the minilib. --- project/JavaLangObject.scala | 10 +++------- project/MiniLib.scala | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/project/JavaLangObject.scala b/project/JavaLangObject.scala index 8ebc44f2d6..2487e451a9 100644 --- a/project/JavaLangObject.scala +++ b/project/JavaLangObject.scala @@ -63,7 +63,7 @@ object JavaLangObject { GetClass(This()(ThisType)) })(OptimizerHints.empty.withInline(true), None), - /* def hashCode(): Int = System.identityHashCode(this) */ + /* def hashCode(): Int = (this) */ MethodDef( MemberFlags.empty, MethodIdent(MethodName("hashCode", Nil, IntRef)), @@ -71,12 +71,8 @@ object JavaLangObject { Nil, IntType, Some { - Apply( - EAF, - LoadModule(ClassName("java.lang.System$")), - MethodIdent(MethodName("identityHashCode", List(ObjectClassRef), IntRef)), - List(This()(ThisType)))(IntType) - })(OptimizerHints.empty, None), + IdentityHashCode(This()(ThisType)) + })(OptimizerHints.empty.withInline(true), None), /* def equals(that: Object): Boolean = this eq that */ MethodDef( diff --git a/project/MiniLib.scala b/project/MiniLib.scala index 2b873dc5f3..a9b9026765 100644 --- a/project/MiniLib.scala +++ b/project/MiniLib.scala @@ -5,7 +5,6 @@ object MiniLib { val inJavaLang = List( "Object", "Class", - "System", "CharSequence", "Cloneable", From 4b4aa36fa6a58075c400366b8f1eb39f1fef3ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 25 Jul 2022 19:09:20 +0200 Subject: [PATCH 32/75] Add missing `()` to 0-arg Java method calls in javalanglib and javalib. The relevant methods are defined with `()`, so they should be called with `()`. When we merge the javalanglib and javalib, the compiler will require them to align with each other. --- javalanglib/src/main/scala/java/lang/_String.scala | 8 ++++---- javalib/src/main/scala/java/math/RoundingMode.scala | 2 +- javalib/src/main/scala/java/nio/CharBuffer.scala | 4 ++-- javalib/src/main/scala/java/nio/StringCharBuffer.scala | 2 +- javalib/src/main/scala/java/util/AbstractCollection.scala | 4 ++-- javalib/src/main/scala/java/util/AbstractQueue.scala | 2 +- javalib/src/main/scala/java/util/Collections.scala | 2 +- javalib/src/main/scala/java/util/EventObject.scala | 2 +- javalib/src/main/scala/java/util/IdentityHashMap.scala | 2 +- javalib/src/main/scala/java/util/LinkedList.scala | 2 +- javalib/src/main/scala/java/util/Properties.scala | 8 ++++---- javalib/src/main/scala/java/util/Timer.scala | 6 +++--- javalib/src/main/scala/java/util/UUID.scala | 4 ++-- .../scala/java/util/concurrent/CopyOnWriteArrayList.scala | 8 ++++---- .../src/main/scala/java/util/concurrent/TimeUnit.scala | 2 +- 15 files changed, 29 insertions(+), 29 deletions(-) diff --git a/javalanglib/src/main/scala/java/lang/_String.scala b/javalanglib/src/main/scala/java/lang/_String.scala index 71bd4c3c59..07ebf75421 100644 --- a/javalanglib/src/main/scala/java/lang/_String.scala +++ b/javalanglib/src/main/scala/java/lang/_String.scala @@ -212,14 +212,14 @@ final class _String private () // scalastyle:ignore thisString.jsSubstring(this.length() - suffix.length()) == suffix def getBytes(): Array[scala.Byte] = - getBytes(Charset.defaultCharset) + getBytes(Charset.defaultCharset()) def getBytes(charsetName: String): Array[scala.Byte] = getBytes(Charset.forName(charsetName)) def getBytes(charset: Charset): Array[scala.Byte] = { val buf = charset.encode(thisString) - val res = new Array[scala.Byte](buf.remaining) + val res = new Array[scala.Byte](buf.remaining()) buf.get(res) res } @@ -958,7 +958,7 @@ object _String { // scalastyle:ignore } def `new`(bytes: Array[scala.Byte]): String = - `new`(bytes, Charset.defaultCharset) + `new`(bytes, Charset.defaultCharset()) def `new`(bytes: Array[scala.Byte], charsetName: String): String = `new`(bytes, Charset.forName(charsetName)) @@ -967,7 +967,7 @@ object _String { // scalastyle:ignore charset.decode(ByteBuffer.wrap(bytes)).toString() def `new`(bytes: Array[scala.Byte], offset: Int, length: Int): String = - `new`(bytes, offset, length, Charset.defaultCharset) + `new`(bytes, offset, length, Charset.defaultCharset()) def `new`(bytes: Array[scala.Byte], offset: Int, length: Int, charsetName: String): String = diff --git a/javalib/src/main/scala/java/math/RoundingMode.scala b/javalib/src/main/scala/java/math/RoundingMode.scala index afc7c3567a..58d800f71a 100644 --- a/javalib/src/main/scala/java/math/RoundingMode.scala +++ b/javalib/src/main/scala/java/math/RoundingMode.scala @@ -58,7 +58,7 @@ object RoundingMode { var i = 0 while (i != len) { val value = values(i) - if (value.name == name) + if (value.name() == name) return value i += 1 } diff --git a/javalib/src/main/scala/java/nio/CharBuffer.scala b/javalib/src/main/scala/java/nio/CharBuffer.scala index 8501f7a01c..6762cbaa45 100644 --- a/javalib/src/main/scala/java/nio/CharBuffer.scala +++ b/javalib/src/main/scala/java/nio/CharBuffer.scala @@ -27,10 +27,10 @@ object CharBuffer { wrap(array, 0, array.length) def wrap(csq: CharSequence, start: Int, end: Int): CharBuffer = - StringCharBuffer.wrap(csq, 0, csq.length, start, end - start) + StringCharBuffer.wrap(csq, 0, csq.length(), start, end - start) def wrap(csq: CharSequence): CharBuffer = - wrap(csq, 0, csq.length) + wrap(csq, 0, csq.length()) // Extended API diff --git a/javalib/src/main/scala/java/nio/StringCharBuffer.scala b/javalib/src/main/scala/java/nio/StringCharBuffer.scala index ecd3d0e168..241534d7f5 100644 --- a/javalib/src/main/scala/java/nio/StringCharBuffer.scala +++ b/javalib/src/main/scala/java/nio/StringCharBuffer.scala @@ -100,7 +100,7 @@ private[nio] final class StringCharBuffer private ( private[nio] object StringCharBuffer { private[nio] def wrap(csq: CharSequence, csqOffset: Int, capacity: Int, initialPosition: Int, initialLength: Int): CharBuffer = { - if (csqOffset < 0 || capacity < 0 || csqOffset+capacity > csq.length) + if (csqOffset < 0 || capacity < 0 || csqOffset + capacity > csq.length()) throw new IndexOutOfBoundsException val initialLimit = initialPosition + initialLength if (initialPosition < 0 || initialLength < 0 || initialLimit > capacity) diff --git a/javalib/src/main/scala/java/util/AbstractCollection.scala b/javalib/src/main/scala/java/util/AbstractCollection.scala index d514ef55f1..8385d981da 100644 --- a/javalib/src/main/scala/java/util/AbstractCollection.scala +++ b/javalib/src/main/scala/java/util/AbstractCollection.scala @@ -33,9 +33,9 @@ abstract class AbstractCollection[E] protected () extends Collection[E] { def toArray[T <: AnyRef](a: Array[T]): Array[T] = { val toFill: Array[T] = if (a.length >= size()) a - else jlr.Array.newInstance(a.getClass.getComponentType, size()).asInstanceOf[Array[T]] + else jlr.Array.newInstance(a.getClass().getComponentType(), size()).asInstanceOf[Array[T]] - val iter = iterator + val iter = iterator() for (i <- 0 until size()) toFill(i) = iter.next().asInstanceOf[T] if (toFill.length > size()) diff --git a/javalib/src/main/scala/java/util/AbstractQueue.scala b/javalib/src/main/scala/java/util/AbstractQueue.scala index e1eb450d20..913779d91e 100644 --- a/javalib/src/main/scala/java/util/AbstractQueue.scala +++ b/javalib/src/main/scala/java/util/AbstractQueue.scala @@ -32,7 +32,7 @@ abstract class AbstractQueue[E] protected () } override def addAll(c: Collection[_ <: E]): Boolean = { - val iter = c.iterator + val iter = c.iterator() var changed = false while (iter.hasNext()) changed = add(iter.next()) || changed diff --git a/javalib/src/main/scala/java/util/Collections.scala b/javalib/src/main/scala/java/util/Collections.scala index 8b57918ccb..5ec0ab134f 100644 --- a/javalib/src/main/scala/java/util/Collections.scala +++ b/javalib/src/main/scala/java/util/Collections.scala @@ -544,7 +544,7 @@ object Collections { } def enumeration[T](c: Collection[T]): Enumeration[T] = { - val it = c.iterator + val it = c.iterator() new Enumeration[T] { override def hasMoreElements(): Boolean = it.hasNext() diff --git a/javalib/src/main/scala/java/util/EventObject.scala b/javalib/src/main/scala/java/util/EventObject.scala index dfed2519ea..f792217e04 100644 --- a/javalib/src/main/scala/java/util/EventObject.scala +++ b/javalib/src/main/scala/java/util/EventObject.scala @@ -16,5 +16,5 @@ class EventObject(protected var source: AnyRef) { def getSource(): AnyRef = source override def toString(): String = - s"${getClass.getSimpleName}[source=$source]" + s"${getClass().getSimpleName()}[source=$source]" } diff --git a/javalib/src/main/scala/java/util/IdentityHashMap.scala b/javalib/src/main/scala/java/util/IdentityHashMap.scala index 7a7d36a3ec..cb236bf263 100644 --- a/javalib/src/main/scala/java/util/IdentityHashMap.scala +++ b/javalib/src/main/scala/java/util/IdentityHashMap.scala @@ -169,7 +169,7 @@ class IdentityHashMap[K, V] private ( modified } } - removeAll(this.iterator, false) + removeAll(this.iterator(), false) } } diff --git a/javalib/src/main/scala/java/util/LinkedList.scala b/javalib/src/main/scala/java/util/LinkedList.scala index 5354b9dbd0..cd0f205b8d 100644 --- a/javalib/src/main/scala/java/util/LinkedList.scala +++ b/javalib/src/main/scala/java/util/LinkedList.scala @@ -123,7 +123,7 @@ class LinkedList[E]() extends AbstractSequentialList[E] _removeOccurrence(listIterator(), o) override def addAll(c: Collection[_ <: E]): Boolean = { - val iter = c.iterator + val iter = c.iterator() val changed = iter.hasNext() while (iter.hasNext()) addLast(iter.next()) diff --git a/javalib/src/main/scala/java/util/Properties.scala b/javalib/src/main/scala/java/util/Properties.scala index 45e559214b..d75d89e601 100644 --- a/javalib/src/main/scala/java/util/Properties.scala +++ b/javalib/src/main/scala/java/util/Properties.scala @@ -59,7 +59,7 @@ class Properties(protected val defaults: Properties) writer.write('#') writer.write(new Date().toString) - writer.write(System.lineSeparator) + writer.write(System.lineSeparator()) entrySet().scalaOps.foreach { entry => writer.write(encodeString(entry.getKey().asInstanceOf[String], @@ -67,7 +67,7 @@ class Properties(protected val defaults: Properties) writer.write('=') writer.write(encodeString(entry.getValue().asInstanceOf[String], isKey = false, toHex)) - writer.write(System.lineSeparator) + writer.write(System.lineSeparator()) } writer.flush() } @@ -299,7 +299,7 @@ class Properties(protected val defaults: Properties) if (isCrlf) { index += 1 } - writer.write(System.lineSeparator) + writer.write(System.lineSeparator()) def noExplicitComment = { index + 1 < chars.length && @@ -321,7 +321,7 @@ class Properties(protected val defaults: Properties) } index += 1 } - writer.write(System.lineSeparator) + writer.write(System.lineSeparator()) } private def encodeString(string: String, isKey: Boolean, diff --git a/javalib/src/main/scala/java/util/Timer.scala b/javalib/src/main/scala/java/util/Timer.scala index 4be9d67d43..b377b5a33f 100644 --- a/javalib/src/main/scala/java/util/Timer.scala +++ b/javalib/src/main/scala/java/util/Timer.scala @@ -31,7 +31,7 @@ class Timer() { } private def checkDelay(delay: Long): Unit = { - if (delay < 0 || (delay + System.currentTimeMillis) < 0) + if (delay < 0 || (delay + System.currentTimeMillis()) < 0) throw new IllegalArgumentException("Negative delay.") } @@ -106,7 +106,7 @@ class Timer() { def loop(scheduledTime: Long): Unit = { task.doRun() val nextScheduledTime = scheduledTime + period - val nowTime = System.nanoTime / 1000000L + val nowTime = System.nanoTime() / 1000000L if (nowTime >= nextScheduledTime) { // Re-run immediately. loop(nextScheduledTime) @@ -119,7 +119,7 @@ class Timer() { } task.timeout(delay) { - loop(System.nanoTime / 1000000L + period) + loop(System.nanoTime() / 1000000L + period) } } diff --git a/javalib/src/main/scala/java/util/UUID.scala b/javalib/src/main/scala/java/util/UUID.scala index ab08fa3fea..bc8d02f782 100644 --- a/javalib/src/main/scala/java/util/UUID.scala +++ b/javalib/src/main/scala/java/util/UUID.scala @@ -47,13 +47,13 @@ final class UUID private ( def getLeastSignificantBits(): Long = { if (l2 eq null) l2 = JLong.valueOf((i3.toLong << 32) | (i4.toLong & 0xffffffffL)) - l2.longValue + l2.longValue() } def getMostSignificantBits(): Long = { if (l1 eq null) l1 = JLong.valueOf((i1.toLong << 32) | (i2.toLong & 0xffffffffL)) - l1.longValue + l1.longValue() } def version(): Int = diff --git a/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala b/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala index 16d35937fd..994c2acb6d 100644 --- a/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala +++ b/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala @@ -54,7 +54,7 @@ class CopyOnWriteArrayList[E <: AnyRef] private (private var inner: js.Array[E]) size() == 0 def contains(o: scala.Any): Boolean = - iterator.scalaOps.exists(Objects.equals(o, _)) + iterator().scalaOps.exists(Objects.equals(o, _)) def indexOf(o: scala.Any): Int = indexOf(o.asInstanceOf[E], 0) @@ -84,12 +84,12 @@ class CopyOnWriteArrayList[E <: AnyRef] private (private var inner: js.Array[E]) toArray(new Array[AnyRef](size())) def toArray[T <: AnyRef](a: Array[T]): Array[T] = { - val componentType = a.getClass.getComponentType + val componentType = a.getClass().getComponentType() val toFill: Array[T] = if (a.length >= size()) a else jlr.Array.newInstance(componentType, size()).asInstanceOf[Array[T]] - val iter = iterator + val iter = iterator() for (i <- 0 until size()) toFill(i) = iter.next().asInstanceOf[T] if (toFill.length > size()) @@ -145,7 +145,7 @@ class CopyOnWriteArrayList[E <: AnyRef] private (private var inner: js.Array[E]) } def containsAll(c: Collection[_]): Boolean = - c.iterator.scalaOps.forall(this.contains(_)) + c.iterator().scalaOps.forall(this.contains(_)) def removeAll(c: Collection[_]): Boolean = { copyIfNeeded() diff --git a/javalib/src/main/scala/java/util/concurrent/TimeUnit.scala b/javalib/src/main/scala/java/util/concurrent/TimeUnit.scala index e1054d84a1..c308bf1b97 100644 --- a/javalib/src/main/scala/java/util/concurrent/TimeUnit.scala +++ b/javalib/src/main/scala/java/util/concurrent/TimeUnit.scala @@ -134,7 +134,7 @@ object TimeUnit { var i = 0 while (i != len) { val value = values(i) - if (value.name == name) + if (value.name() == name) return value i += 1 } From 1e4e059cd2449324445da50bf58f46c762649f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 25 Jul 2022 19:11:13 +0200 Subject: [PATCH 33/75] Merge the javalanglib into the javalib. They were separated in fb590e6f8b710186ec2cd4e1b58af09daf7bb734 because scalac could not handle compiling them together (for some obscure reason related to hijacked classes). Later, we introduced the IR cleaner on the javalanglib, and so they had to be kept separate as it did not apply to the javalib. Now that IR cleaner is also applied to the javalib, we can merge them back together. Apparently, all the versions of scalac that we still support can handle it. The merge prompts 3 kinds of changes in the source code: * Using Any instead of Object as parameter of some methods that are called with unconstrained `T` (this is common practice, and corresponds to the fact that scalac interprets `j.l.Object` in Java signatures as `Any`. * Butcher the source signature of `Collections.{min,max}` a bit more, because `j.l.Comparable` is now considered a subtype of `AnyRef`, so Scala's erasure would still choose `Comparable` over `Object`, but Java's erasure requires `Object`. * Calls to varargs methods now appear to take `Array`s instead of varargs. --- build.sbt | 1 - .../src/main/scala/java/lang/Appendable.scala | 0 .../main/scala/java/lang/AutoCloseable.scala | 0 .../src/main/scala/java/lang/Boolean.scala | 0 .../src/main/scala/java/lang/Byte.scala | 0 .../main/scala/java/lang/CharSequence.scala | 0 .../src/main/scala/java/lang/Character.scala | 0 .../src/main/scala/java/lang/Class.scala | 8 +-- .../main/scala/java/lang/ClassLoader.scala | 0 .../src/main/scala/java/lang/ClassValue.scala | 0 .../src/main/scala/java/lang/Cloneable.scala | 0 .../src/main/scala/java/lang/Comparable.scala | 0 .../src/main/scala/java/lang/Double.scala | 0 .../src/main/scala/java/lang/Enum.scala | 0 .../src/main/scala/java/lang/Float.scala | 0 .../scala/java/lang/FloatingPointBits.scala | 0 .../java/lang/InheritableThreadLocal.scala | 0 .../src/main/scala/java/lang/Integer.scala | 0 .../src/main/scala/java/lang/Iterable.scala | 0 .../src/main/scala/java/lang/Long.scala | 0 .../src/main/scala/java/lang/Math.scala | 0 .../src/main/scala/java/lang/Number.scala | 0 .../src/main/scala/java/lang/Readable.scala | 0 .../src/main/scala/java/lang/Runnable.scala | 0 .../src/main/scala/java/lang/Runtime.scala | 0 .../src/main/scala/java/lang/Short.scala | 0 .../src/main/scala/java/lang/StackTrace.scala | 0 .../scala/java/lang/StackTraceElement.scala | 0 .../main/scala/java/lang/StringBuffer.scala | 0 .../main/scala/java/lang/StringBuilder.scala | 0 .../src/main/scala/java/lang/System.scala | 4 +- .../src/main/scala/java/lang/Thread.scala | 0 .../main/scala/java/lang/ThreadLocal.scala | 0 .../src/main/scala/java/lang/Throwables.scala | 0 .../src/main/scala/java/lang/Utils.scala | 0 .../src/main/scala/java/lang/Void.scala | 0 .../src/main/scala/java/lang/_String.scala | 4 +- .../java/lang/annotation/Annotation.scala | 0 .../scala/java/lang/constant/Constable.scala | 0 .../java/lang/constant/ConstantDesc.scala | 0 .../main/scala/java/lang/reflect/Array.scala | 0 .../main/scala/java/util/Collections.scala | 8 +-- project/Build.scala | 60 ++++++------------- 43 files changed, 30 insertions(+), 55 deletions(-) rename {javalanglib => javalib}/src/main/scala/java/lang/Appendable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/AutoCloseable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Boolean.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Byte.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/CharSequence.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Character.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Class.scala (95%) rename {javalanglib => javalib}/src/main/scala/java/lang/ClassLoader.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/ClassValue.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Cloneable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Comparable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Double.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Enum.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Float.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/FloatingPointBits.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/InheritableThreadLocal.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Integer.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Iterable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Long.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Math.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Number.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Readable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Runnable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Runtime.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Short.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/StackTrace.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/StackTraceElement.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/StringBuffer.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/StringBuilder.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/System.scala (99%) rename {javalanglib => javalib}/src/main/scala/java/lang/Thread.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/ThreadLocal.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Throwables.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Utils.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/Void.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/_String.scala (99%) rename {javalanglib => javalib}/src/main/scala/java/lang/annotation/Annotation.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/constant/Constable.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/constant/ConstantDesc.scala (100%) rename {javalanglib => javalib}/src/main/scala/java/lang/reflect/Array.scala (100%) diff --git a/build.sbt b/build.sbt index e56269acab..73bfa4a655 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,6 @@ val linker = Build.linker val linkerJS = Build.linkerJS val testAdapter = Build.testAdapter val sbtPlugin = Build.plugin -val javalanglib = Build.javalanglib val javalib = Build.javalib val scalalib = Build.scalalib val libraryAux = Build.libraryAux diff --git a/javalanglib/src/main/scala/java/lang/Appendable.scala b/javalib/src/main/scala/java/lang/Appendable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Appendable.scala rename to javalib/src/main/scala/java/lang/Appendable.scala diff --git a/javalanglib/src/main/scala/java/lang/AutoCloseable.scala b/javalib/src/main/scala/java/lang/AutoCloseable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/AutoCloseable.scala rename to javalib/src/main/scala/java/lang/AutoCloseable.scala diff --git a/javalanglib/src/main/scala/java/lang/Boolean.scala b/javalib/src/main/scala/java/lang/Boolean.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Boolean.scala rename to javalib/src/main/scala/java/lang/Boolean.scala diff --git a/javalanglib/src/main/scala/java/lang/Byte.scala b/javalib/src/main/scala/java/lang/Byte.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Byte.scala rename to javalib/src/main/scala/java/lang/Byte.scala diff --git a/javalanglib/src/main/scala/java/lang/CharSequence.scala b/javalib/src/main/scala/java/lang/CharSequence.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/CharSequence.scala rename to javalib/src/main/scala/java/lang/CharSequence.scala diff --git a/javalanglib/src/main/scala/java/lang/Character.scala b/javalib/src/main/scala/java/lang/Character.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Character.scala rename to javalib/src/main/scala/java/lang/Character.scala diff --git a/javalanglib/src/main/scala/java/lang/Class.scala b/javalib/src/main/scala/java/lang/Class.scala similarity index 95% rename from javalanglib/src/main/scala/java/lang/Class.scala rename to javalib/src/main/scala/java/lang/Class.scala index 06c4f7a3a3..db5cc45ec3 100644 --- a/javalanglib/src/main/scala/java/lang/Class.scala +++ b/javalib/src/main/scala/java/lang/Class.scala @@ -21,9 +21,9 @@ private trait ScalaJSClassData[A] extends js.Object { val isInterface: scala.Boolean = js.native val isArrayClass: scala.Boolean = js.native - def isInstance(obj: Object): scala.Boolean = js.native + def isInstance(obj: Any): scala.Boolean = js.native def isAssignableFrom(that: ScalaJSClassData[_]): scala.Boolean = js.native - def checkCast(obj: Object): scala.Unit = js.native + def checkCast(obj: Any): scala.Unit = js.native def getSuperclass(): Class[_ >: A] = js.native def getComponentType(): Class[_] = js.native @@ -55,7 +55,7 @@ final class Class[A] private (data0: Object) extends Object { if (isPrimitive()) "" else "class ")+getName() } - def isInstance(obj: Object): scala.Boolean = + def isInstance(obj: Any): scala.Boolean = data.isInstance(obj) def isAssignableFrom(that: Class[_]): scala.Boolean = @@ -141,7 +141,7 @@ final class Class[A] private (data0: Object) extends Object { data.getComponentType() @inline - def cast(obj: Object): A = { + def cast(obj: Any): A = { getData().checkCast(obj) obj.asInstanceOf[A] } diff --git a/javalanglib/src/main/scala/java/lang/ClassLoader.scala b/javalib/src/main/scala/java/lang/ClassLoader.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/ClassLoader.scala rename to javalib/src/main/scala/java/lang/ClassLoader.scala diff --git a/javalanglib/src/main/scala/java/lang/ClassValue.scala b/javalib/src/main/scala/java/lang/ClassValue.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/ClassValue.scala rename to javalib/src/main/scala/java/lang/ClassValue.scala diff --git a/javalanglib/src/main/scala/java/lang/Cloneable.scala b/javalib/src/main/scala/java/lang/Cloneable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Cloneable.scala rename to javalib/src/main/scala/java/lang/Cloneable.scala diff --git a/javalanglib/src/main/scala/java/lang/Comparable.scala b/javalib/src/main/scala/java/lang/Comparable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Comparable.scala rename to javalib/src/main/scala/java/lang/Comparable.scala diff --git a/javalanglib/src/main/scala/java/lang/Double.scala b/javalib/src/main/scala/java/lang/Double.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Double.scala rename to javalib/src/main/scala/java/lang/Double.scala diff --git a/javalanglib/src/main/scala/java/lang/Enum.scala b/javalib/src/main/scala/java/lang/Enum.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Enum.scala rename to javalib/src/main/scala/java/lang/Enum.scala diff --git a/javalanglib/src/main/scala/java/lang/Float.scala b/javalib/src/main/scala/java/lang/Float.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Float.scala rename to javalib/src/main/scala/java/lang/Float.scala diff --git a/javalanglib/src/main/scala/java/lang/FloatingPointBits.scala b/javalib/src/main/scala/java/lang/FloatingPointBits.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/FloatingPointBits.scala rename to javalib/src/main/scala/java/lang/FloatingPointBits.scala diff --git a/javalanglib/src/main/scala/java/lang/InheritableThreadLocal.scala b/javalib/src/main/scala/java/lang/InheritableThreadLocal.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/InheritableThreadLocal.scala rename to javalib/src/main/scala/java/lang/InheritableThreadLocal.scala diff --git a/javalanglib/src/main/scala/java/lang/Integer.scala b/javalib/src/main/scala/java/lang/Integer.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Integer.scala rename to javalib/src/main/scala/java/lang/Integer.scala diff --git a/javalanglib/src/main/scala/java/lang/Iterable.scala b/javalib/src/main/scala/java/lang/Iterable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Iterable.scala rename to javalib/src/main/scala/java/lang/Iterable.scala diff --git a/javalanglib/src/main/scala/java/lang/Long.scala b/javalib/src/main/scala/java/lang/Long.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Long.scala rename to javalib/src/main/scala/java/lang/Long.scala diff --git a/javalanglib/src/main/scala/java/lang/Math.scala b/javalib/src/main/scala/java/lang/Math.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Math.scala rename to javalib/src/main/scala/java/lang/Math.scala diff --git a/javalanglib/src/main/scala/java/lang/Number.scala b/javalib/src/main/scala/java/lang/Number.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Number.scala rename to javalib/src/main/scala/java/lang/Number.scala diff --git a/javalanglib/src/main/scala/java/lang/Readable.scala b/javalib/src/main/scala/java/lang/Readable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Readable.scala rename to javalib/src/main/scala/java/lang/Readable.scala diff --git a/javalanglib/src/main/scala/java/lang/Runnable.scala b/javalib/src/main/scala/java/lang/Runnable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Runnable.scala rename to javalib/src/main/scala/java/lang/Runnable.scala diff --git a/javalanglib/src/main/scala/java/lang/Runtime.scala b/javalib/src/main/scala/java/lang/Runtime.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Runtime.scala rename to javalib/src/main/scala/java/lang/Runtime.scala diff --git a/javalanglib/src/main/scala/java/lang/Short.scala b/javalib/src/main/scala/java/lang/Short.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Short.scala rename to javalib/src/main/scala/java/lang/Short.scala diff --git a/javalanglib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/StackTrace.scala rename to javalib/src/main/scala/java/lang/StackTrace.scala diff --git a/javalanglib/src/main/scala/java/lang/StackTraceElement.scala b/javalib/src/main/scala/java/lang/StackTraceElement.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/StackTraceElement.scala rename to javalib/src/main/scala/java/lang/StackTraceElement.scala diff --git a/javalanglib/src/main/scala/java/lang/StringBuffer.scala b/javalib/src/main/scala/java/lang/StringBuffer.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/StringBuffer.scala rename to javalib/src/main/scala/java/lang/StringBuffer.scala diff --git a/javalanglib/src/main/scala/java/lang/StringBuilder.scala b/javalib/src/main/scala/java/lang/StringBuilder.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/StringBuilder.scala rename to javalib/src/main/scala/java/lang/StringBuilder.scala diff --git a/javalanglib/src/main/scala/java/lang/System.scala b/javalib/src/main/scala/java/lang/System.scala similarity index 99% rename from javalanglib/src/main/scala/java/lang/System.scala rename to javalib/src/main/scala/java/lang/System.scala index 5b34a6680c..ce89854b10 100644 --- a/javalanglib/src/main/scala/java/lang/System.scala +++ b/javalib/src/main/scala/java/lang/System.scala @@ -182,8 +182,8 @@ object System { } @inline - def identityHashCode(x: Object): scala.Int = - scala.scalajs.runtime.identityHashCode(x) + def identityHashCode(x: Any): scala.Int = + scala.scalajs.runtime.identityHashCode(x.asInstanceOf[AnyRef]) // System properties -------------------------------------------------------- diff --git a/javalanglib/src/main/scala/java/lang/Thread.scala b/javalib/src/main/scala/java/lang/Thread.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Thread.scala rename to javalib/src/main/scala/java/lang/Thread.scala diff --git a/javalanglib/src/main/scala/java/lang/ThreadLocal.scala b/javalib/src/main/scala/java/lang/ThreadLocal.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/ThreadLocal.scala rename to javalib/src/main/scala/java/lang/ThreadLocal.scala diff --git a/javalanglib/src/main/scala/java/lang/Throwables.scala b/javalib/src/main/scala/java/lang/Throwables.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Throwables.scala rename to javalib/src/main/scala/java/lang/Throwables.scala diff --git a/javalanglib/src/main/scala/java/lang/Utils.scala b/javalib/src/main/scala/java/lang/Utils.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Utils.scala rename to javalib/src/main/scala/java/lang/Utils.scala diff --git a/javalanglib/src/main/scala/java/lang/Void.scala b/javalib/src/main/scala/java/lang/Void.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/Void.scala rename to javalib/src/main/scala/java/lang/Void.scala diff --git a/javalanglib/src/main/scala/java/lang/_String.scala b/javalib/src/main/scala/java/lang/_String.scala similarity index 99% rename from javalanglib/src/main/scala/java/lang/_String.scala rename to javalib/src/main/scala/java/lang/_String.scala index 07ebf75421..5338da085d 100644 --- a/javalanglib/src/main/scala/java/lang/_String.scala +++ b/javalib/src/main/scala/java/lang/_String.scala @@ -1022,9 +1022,9 @@ object _String { // scalastyle:ignore `new`(data, offset, count) def format(format: String, args: Array[AnyRef]): String = - new java.util.Formatter().format(format, args: _*).toString() + new java.util.Formatter().format(format, args).toString() def format(l: Locale, format: String, args: Array[AnyRef]): String = - new java.util.Formatter(l).format(format, args: _*).toString() + new java.util.Formatter(l).format(format, args).toString() } diff --git a/javalanglib/src/main/scala/java/lang/annotation/Annotation.scala b/javalib/src/main/scala/java/lang/annotation/Annotation.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/annotation/Annotation.scala rename to javalib/src/main/scala/java/lang/annotation/Annotation.scala diff --git a/javalanglib/src/main/scala/java/lang/constant/Constable.scala b/javalib/src/main/scala/java/lang/constant/Constable.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/constant/Constable.scala rename to javalib/src/main/scala/java/lang/constant/Constable.scala diff --git a/javalanglib/src/main/scala/java/lang/constant/ConstantDesc.scala b/javalib/src/main/scala/java/lang/constant/ConstantDesc.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/constant/ConstantDesc.scala rename to javalib/src/main/scala/java/lang/constant/ConstantDesc.scala diff --git a/javalanglib/src/main/scala/java/lang/reflect/Array.scala b/javalib/src/main/scala/java/lang/reflect/Array.scala similarity index 100% rename from javalanglib/src/main/scala/java/lang/reflect/Array.scala rename to javalib/src/main/scala/java/lang/reflect/Array.scala diff --git a/javalib/src/main/scala/java/util/Collections.scala b/javalib/src/main/scala/java/util/Collections.scala index 5ec0ab134f..e27b526a86 100644 --- a/javalib/src/main/scala/java/util/Collections.scala +++ b/javalib/src/main/scala/java/util/Collections.scala @@ -258,15 +258,15 @@ object Collections { } } - // Differs from original type definition, original: [T <: jl.Comparable[_ >: T]] - def min[T <: AnyRef with jl.Comparable[T]](coll: Collection[_ <: T]): T = + // Differs from original type definition, original: [T <: jl.Comparable[_ >: T]], returning T + def min[T <: AnyRef with jl.Comparable[T]](coll: Collection[_ <: T]): AnyRef = min(coll, naturalComparator[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) - // Differs from original type definition, original: [T <: jl.Comparable[_ >: T]] - def max[T <: AnyRef with jl.Comparable[T]](coll: Collection[_ <: T]): T = + // 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, naturalComparator[T]) def max[T](coll: Collection[_ <: T], comp: Comparator[_ >: T]): T = diff --git a/project/Build.scala b/project/Build.scala index 2a61950768..320b8eb440 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -712,7 +712,7 @@ object Build { compiler, irProject, irProjectJS, linkerInterface, linkerInterfaceJS, linker, linkerJS, testAdapter, - javalanglib, javalib, scalalib, libraryAux, library, + javalib, scalalib, libraryAux, library, testInterface, jUnitRuntime, testBridge, jUnitPlugin, jUnitAsyncJS, jUnitAsyncJVM, jUnitTestOutputsJS, jUnitTestOutputsJVM, helloworld, reversi, testingExample, testSuite, testSuiteJVM, @@ -1194,44 +1194,6 @@ object Build { } } - lazy val javalanglib: MultiScalaProject = MultiScalaProject( - id = "javalanglib", base = file("javalanglib") - ).enablePlugins( - MyScalaJSPlugin - ).settings( - commonSettings, - fatalWarningsSettings, - name := "java.lang library for Scala.js", - publishArtifact in Compile := false, - delambdafySetting, - ensureSAMSupportSetting, - - recompileAllOrNothingSettings, - - /* When writing code in the java.lang package, references to things - * like `Boolean` or `Double` refer to `j.l.Boolean` or `j.l.Double`. - * Usually this is not what we want (we want the primitive types - * instead), but the implicits available in `Predef` hide mistakes by - * introducing boxing and unboxing where required. The `-Yno-predef` - * flag prevents these mistakes from happening. - */ - scalacOptions += "-Yno-predef", - // We implement JDK classes, so we emit static forwarders for all static objects - scalacOptions ++= scalaJSCompilerOption("genStaticForwardersForNonTopLevelObjects"), - - resourceGenerators in Compile += Def.task { - val output = (resourceManaged in Compile).value / "java/lang/Object.sjsir" - val data = JavaLangObject.irBytes - - if (!output.exists || !Arrays.equals(data, IO.readBytes(output))) { - IO.write(output, data) - } - - Seq(output) - }.taskValue, - cleanIRSettings, - ).withScalaJSCompiler.dependsOnLibraryNoJar - lazy val javalib: MultiScalaProject = MultiScalaProject( id = "javalib", base = file("javalib") ).enablePlugins( @@ -1248,6 +1210,12 @@ object Build { /* Do not import `Predef._` so that we have a better control of when * we rely on the Scala library. + * This is particularly important within the java.lang package, as + * references to things like `Boolean` or `Double` refer to `j.l.Boolean` + * or `j.l.Double`. Usually this is not what we want (we want the + * primitive types instead), but the implicits available in `Predef` + * hide mistakes by introducing boxing and unboxing where required. + * The `-Yno-predef` flag prevents these mistakes from happening. */ scalacOptions += "-Yno-predef", // We implement JDK classes, so we emit static forwarders for all static objects @@ -1271,6 +1239,15 @@ object Build { Nil }, + // The implementation of java.lang.Object, which is hard-coded in JavaLangObject.scala + resourceGenerators in Compile += Def.task { + val output = (resourceManaged in Compile).value / "java/lang/Object.sjsir" + val data = JavaLangObject.irBytes + if (!output.exists || !Arrays.equals(data, IO.readBytes(output))) + IO.write(output, data) + Seq(output) + }.taskValue, + cleanIRSettings, headerSources in Compile ~= { srcs => @@ -1522,7 +1499,7 @@ object Build { */ dependencyClasspath in doc ++= exportedProducts.value, )) - ).zippedSettings(Seq("javalanglib", "javalib", "scalalib", "libraryAux"))(localProjects => + ).zippedSettings(Seq("javalib", "scalalib", "libraryAux"))(localProjects => inConfig(Compile)(Seq( /* Add the .sjsir files from other lib projects * (but not .class files) @@ -1543,8 +1520,7 @@ object Build { val otherProducts = ( (products in localProjects(0)).value ++ (products in localProjects(1)).value ++ - (products in localProjects(2)).value ++ - (products in localProjects(3)).value) + (products in localProjects(2)).value) val otherMappings = otherProducts.flatMap(base => Path.selectSubpaths(base, filter)) From 8460bc3a039730e526c94a228ff401f502fecc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 25 Jul 2022 19:17:45 +0200 Subject: [PATCH 34/75] Use String.repeat from PatternSyntaxException.getMessage. This was left as a TODO before. Since `repeat` is a JDK 11 method, and `PatternSyntaxException` was in javalib, it previously could not see the `def repeat` that was in javalanglib. Now everything is in the same project, so we can access it. The implementation in `PatternSyntaxException` had an optimization for ES 2015 that `String.repeat` did not have. We add that optimization in `String.repeat` in this commit as well. --- javalib/src/main/scala/java/lang/_String.scala | 6 ++++++ .../util/regex/PatternSyntaxException.scala | 18 +----------------- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +++--- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/javalib/src/main/scala/java/lang/_String.scala b/javalib/src/main/scala/java/lang/_String.scala index 5338da085d..8bbc49f136 100644 --- a/javalib/src/main/scala/java/lang/_String.scala +++ b/javalib/src/main/scala/java/lang/_String.scala @@ -313,6 +313,12 @@ final class _String private () // scalastyle:ignore def repeat(count: Int): String = { if (count < 0) { throw new IllegalArgumentException + } 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. + */ + this.asInstanceOf[js.Dynamic].repeat(count).asInstanceOf[String] } else if (thisString == "" || count == 0) { "" } else if (thisString.length > (Int.MaxValue / count)) { diff --git a/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala b/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala index 99937b1550..945753d91b 100644 --- a/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala +++ b/javalib/src/main/scala/java/util/regex/PatternSyntaxException.scala @@ -34,24 +34,8 @@ class PatternSyntaxException(desc: String, regex: String, index: Int) val base = desc + indexHint + "\n" + re if (idx >= 0 && re != null && idx < re.length()) - base + "\n" + repeat(" ", idx) + "^" + base + "\n" + " ".asInstanceOf[java.lang._String].repeat(idx) + "^" else base } - - @inline - private def repeat(s: String, count: Int): String = { - // TODO Use java.lang.String.repeat() once we can (JDK 11+ method) - if (linkingInfo.esVersion >= LinkingInfo.ESVersion.ES2015) { - s.asInstanceOf[js.Dynamic].repeat(count).asInstanceOf[String] - } else { - var result = "" - var i = 0 - while (i != count) { - result += s - i += 1 - } - result - } - } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 571e1f0656..d5da6629d2 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 144675, - expectedFullLinkSizeWithoutClosure = 133818, - expectedFullLinkSizeWithClosure = 21620, + expectedFastLinkSize = 145210, + expectedFullLinkSizeWithoutClosure = 134353, + expectedFullLinkSizeWithClosure = 21671, classDefs, moduleInitializers = MainTestModuleInitializers ) From f5dd5f85787792d6486c691eeecdbc010b454108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 25 Jul 2022 19:35:46 +0200 Subject: [PATCH 35/75] Merge java.util.JSUtils into java.lang.Utils. Since they are now compiled together, we only need one copy of these utilities. In some cases, the APIs differed a bit. They were reconciled in favor of `java.util.JSUtils`, since that caused the least impact on existing code. --- .../src/main/scala/java/lang/ClassValue.scala | 24 +-- .../src/main/scala/java/lang/StackTrace.scala | 2 +- javalib/src/main/scala/java/lang/System.scala | 4 +- javalib/src/main/scala/java/lang/Utils.scala | 73 ++++++- javalib/src/main/scala/java/net/URI.scala | 2 +- .../main/scala/java/nio/charset/Charset.scala | 2 +- .../scala/java/nio/charset/CoderResult.scala | 2 +- .../src/main/scala/java/util/ArrayDeque.scala | 2 +- .../src/main/scala/java/util/ArrayList.scala | 2 +- .../src/main/scala/java/util/Formatter.scala | 2 +- .../src/main/scala/java/util/JSUtils.scala | 182 ------------------ .../concurrent/CopyOnWriteArrayList.scala | 2 +- .../java/util/regex/IndicesBuilder.scala | 2 +- .../main/scala/java/util/regex/Matcher.scala | 2 +- .../main/scala/java/util/regex/Pattern.scala | 2 +- .../java/util/regex/PatternCompiler.scala | 2 +- project/Build.scala | 4 +- 17 files changed, 90 insertions(+), 221 deletions(-) delete mode 100644 javalib/src/main/scala/java/util/JSUtils.scala diff --git a/javalib/src/main/scala/java/lang/ClassValue.scala b/javalib/src/main/scala/java/lang/ClassValue.scala index eae2eff71a..94f46965e4 100644 --- a/javalib/src/main/scala/java/lang/ClassValue.scala +++ b/javalib/src/main/scala/java/lang/ClassValue.scala @@ -49,24 +49,16 @@ abstract class ClassValue[T] protected () { protected def computeValue(`type`: Class[_]): T def get(`type`: Class[_]): T = { - /* We first perform `get`, and if the result is undefined/null, we use - * `has` to disambiguate a present undefined/null from an absent key. - * Since the purpose of ClassValue is to be used a cache indexed by Class - * values, the expected use case will have more hits than misses, and so - * this ordering should be faster on average than first performing `has` - * then `get`. - */ if (useJSMap) { - undefOrGetOrElse(mapGet(jsMap, `type`)) { - if (mapHas(jsMap, `type`)) { - ().asInstanceOf[T] - } else { - val newValue = computeValue(`type`) - mapSet(jsMap, `type`, newValue) - newValue - } - } + 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. + * Since the purpose of ClassValue is to be used a cache indexed by Class + * values, the expected use case will have more hits than misses, and so + * this ordering should be faster on average than first performing `has` + * then `get`. + */ javaMap.get(`type`) match { case null => if (javaMap.containsKey(`type`)) { diff --git a/javalib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala index 66c3a6dac8..465dd2e29a 100644 --- a/javalib/src/main/scala/java/lang/StackTrace.scala +++ b/javalib/src/main/scala/java/lang/StackTrace.scala @@ -451,7 +451,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 diff --git a/javalib/src/main/scala/java/lang/System.scala b/javalib/src/main/scala/java/lang/System.scala index ce89854b10..c7424a218a 100644 --- a/javalib/src/main/scala/java/lang/System.scala +++ b/javalib/src/main/scala/java/lang/System.scala @@ -233,11 +233,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/Utils.scala b/javalib/src/main/scala/java/lang/Utils.scala index 19652a9337..b57bc2a520 100644 --- a/javalib/src/main/scala/java/lang/Utils.scala +++ b/javalib/src/main/scala/java/lang/Utils.scala @@ -15,9 +15,12 @@ package java.lang import scala.language.implicitConversions import scala.scalajs.js -import scala.scalajs.js.annotation.JSBracketAccess +import scala.scalajs.js.annotation._ + +private[java] object Utils { + @inline + def undefined: js.UndefOr[Nothing] = ().asInstanceOf[js.UndefOr[Nothing]] -private[lang] object Utils { @inline def isUndefined(x: Any): scala.Boolean = x.asInstanceOf[AnyRef] eq ().asInstanceOf[AnyRef] @@ -36,7 +39,18 @@ private[lang] object Utils { else default @inline - def undefOrFold[A, B](x: js.UndefOr[A])(default: => B, f: A => B): B = + def undefOrGetOrNull[A >: Null](x: js.UndefOr[A]): A = + if (undefOrIsDefined(x)) undefOrForceGet(x) + else null + + @inline + def undefOrForeach[A](x: js.UndefOr[A])(f: A => Any): Unit = { + if (undefOrIsDefined(x)) + f(undefOrForceGet(x)) + } + + @inline + def undefOrFold[A, B](x: js.UndefOr[A])(default: => B)(f: A => B): B = if (undefOrIsDefined(x)) f(undefOrForceGet(x)) else default @@ -64,8 +78,13 @@ private[lang] object Utils { def rawUpdate(key: String, value: A): Unit = js.native } - def dictGetOrElse[A](dict: js.Dictionary[_ <: A], key: String, - default: A): A = { + @inline + def dictEmpty[A](): js.Dictionary[A] = + new js.Object().asInstanceOf[js.Dictionary[A]] + + @inline + def dictGetOrElse[A](dict: js.Dictionary[_ <: A], key: String)( + default: => A): A = { if (dictContains(dict, key)) dictRawApply(dict, key) else @@ -101,8 +120,10 @@ private[lang] object Utils { @js.native private trait MapRaw[K, V] extends js.Object { def has(key: K): scala.Boolean = js.native - def get(key: K): js.UndefOr[V] = js.native + def get(key: K): V = js.native + @JSName("get") def getOrUndefined(key: K): js.UndefOr[V] = js.native def set(key: K, value: V): Unit = js.native + def keys(): js.Iterator[K] = js.native } @inline @@ -110,13 +131,29 @@ private[lang] object Utils { map.asInstanceOf[MapRaw[K, V]].has(key) @inline - def mapGet[K, V](map: js.Map[K, V], key: K): js.UndefOr[V] = + def mapGet[K, V](map: js.Map[K, V], key: K): V = map.asInstanceOf[MapRaw[K, V]].get(key) @inline def mapSet[K, V](map: js.Map[K, V], key: K, value: V): Unit = map.asInstanceOf[MapRaw[K, V]].set(key, value) + @inline + def mapGetOrElse[K, V](map: js.Map[K, V], key: K)(default: => V): V = { + val value = map.asInstanceOf[MapRaw[K, V]].getOrUndefined(key) + if (!isUndefined(value) || mapHas(map, key)) value.asInstanceOf[V] + else default + } + + @inline + def mapGetOrElseUpdate[K, V](map: js.Map[K, V], key: K)(default: => V): V = { + mapGetOrElse(map, key) { + val value = default + mapSet(map, key, value) + value + } + } + @inline def forArrayElems[A](array: js.Array[A])(f: A => Any): Unit = { val len = array.length @@ -127,6 +164,28 @@ private[lang] object Utils { } } + @inline + def arrayRemove[A](array: js.Array[A], index: Int): Unit = + array.splice(index, 1) + + @inline + def arrayRemoveAndGet[A](array: js.Array[A], index: Int): A = + array.splice(index, 1)(0) + + @inline + def arrayExists[A](array: js.Array[A])(f: A => scala.Boolean): scala.Boolean = { + // scalastyle:off return + val len = array.length + var i = 0 + while (i != len) { + if (f(array(i))) + return true + i += 1 + } + false + // scalastyle:on return + } + @inline def toUint(x: scala.Double): scala.Double = { import js.DynamicImplicits.number2dynamic (x >>> 0).asInstanceOf[scala.Double] diff --git a/javalib/src/main/scala/java/net/URI.scala b/javalib/src/main/scala/java/net/URI.scala index e114fe6ac7..cb19355b36 100644 --- a/javalib/src/main/scala/java/net/URI.scala +++ b/javalib/src/main/scala/java/net/URI.scala @@ -17,9 +17,9 @@ import scala.scalajs.js import scala.annotation.tailrec +import java.lang.Utils._ import java.nio._ import java.nio.charset.{CodingErrorAction, StandardCharsets} -import java.util.JSUtils._ final class URI(origStr: String) extends Serializable with Comparable[URI] { diff --git a/javalib/src/main/scala/java/nio/charset/Charset.scala b/javalib/src/main/scala/java/nio/charset/Charset.scala index c053e242ba..2c5ac9e42c 100644 --- a/javalib/src/main/scala/java/nio/charset/Charset.scala +++ b/javalib/src/main/scala/java/nio/charset/Charset.scala @@ -12,10 +12,10 @@ package java.nio.charset +import java.lang.Utils._ import java.nio.{ByteBuffer, CharBuffer} import java.util.{Collections, HashSet, Arrays} import java.util.ScalaOps._ -import java.util.JSUtils._ import scala.scalajs.js diff --git a/javalib/src/main/scala/java/nio/charset/CoderResult.scala b/javalib/src/main/scala/java/nio/charset/CoderResult.scala index 257dd0904b..4cbb5a9fd0 100644 --- a/javalib/src/main/scala/java/nio/charset/CoderResult.scala +++ b/javalib/src/main/scala/java/nio/charset/CoderResult.scala @@ -14,8 +14,8 @@ package java.nio.charset import scala.annotation.switch +import java.lang.Utils._ import java.nio._ -import java.util.JSUtils._ import scala.scalajs.js diff --git a/javalib/src/main/scala/java/util/ArrayDeque.scala b/javalib/src/main/scala/java/util/ArrayDeque.scala index 46ef2388a8..f33125df77 100644 --- a/javalib/src/main/scala/java/util/ArrayDeque.scala +++ b/javalib/src/main/scala/java/util/ArrayDeque.scala @@ -13,7 +13,7 @@ package java.util import java.lang.Cloneable -import java.util.JSUtils._ +import java.lang.Utils._ import scala.scalajs.js diff --git a/javalib/src/main/scala/java/util/ArrayList.scala b/javalib/src/main/scala/java/util/ArrayList.scala index ad0e9b2d19..68b9705f62 100644 --- a/javalib/src/main/scala/java/util/ArrayList.scala +++ b/javalib/src/main/scala/java/util/ArrayList.scala @@ -13,7 +13,7 @@ package java.util import java.lang.Cloneable -import java.util.JSUtils._ +import java.lang.Utils._ import scala.scalajs._ diff --git a/javalib/src/main/scala/java/util/Formatter.scala b/javalib/src/main/scala/java/util/Formatter.scala index 5807a2ddcf..7625e4fa44 100644 --- a/javalib/src/main/scala/java/util/Formatter.scala +++ b/javalib/src/main/scala/java/util/Formatter.scala @@ -16,9 +16,9 @@ import scala.annotation.switch import scala.scalajs.js import java.lang.{Double => JDouble} +import java.lang.Utils._ import java.io._ import java.math.{BigDecimal, BigInteger} -import java.util.JSUtils._ final class Formatter private (private[this] var dest: Appendable, formatterLocaleInfo: Formatter.LocaleInfo) diff --git a/javalib/src/main/scala/java/util/JSUtils.scala b/javalib/src/main/scala/java/util/JSUtils.scala deleted file mode 100644 index 0f7d3ab22f..0000000000 --- a/javalib/src/main/scala/java/util/JSUtils.scala +++ /dev/null @@ -1,182 +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.util - -import scala.language.implicitConversions - -import scala.scalajs.js -import scala.scalajs.js.annotation.JSBracketAccess - -private[java] object JSUtils { - @inline - def undefined: js.UndefOr[Nothing] = ().asInstanceOf[js.UndefOr[Nothing]] - - @inline - def isUndefined(x: Any): scala.Boolean = - x.asInstanceOf[AnyRef] eq ().asInstanceOf[AnyRef] - - @inline - def undefOrIsDefined[A](x: js.UndefOr[A]): scala.Boolean = - x ne ().asInstanceOf[AnyRef] - - @inline - def undefOrForceGet[A](x: js.UndefOr[A]): A = - x.asInstanceOf[A] - - @inline - def undefOrGetOrElse[A](x: js.UndefOr[A])(default: => A): A = - if (undefOrIsDefined(x)) x.asInstanceOf[A] - else default - - @inline - def undefOrGetOrNull[A >: Null](x: js.UndefOr[A]): A = - if (undefOrIsDefined(x)) x.asInstanceOf[A] - else null - - @inline - def undefOrForeach[A](x: js.UndefOr[A])(f: A => Any): Unit = { - if (undefOrIsDefined(x)) - f(undefOrForceGet(x)) - } - - @inline - def undefOrFold[A, B](x: js.UndefOr[A])(default: => B)(f: A => B): B = - if (undefOrIsDefined(x)) f(undefOrForceGet(x)) - else default - - private object Cache { - val safeHasOwnProperty = - js.Dynamic.global.Object.prototype.hasOwnProperty - .asInstanceOf[js.ThisFunction1[js.Dictionary[_], String, scala.Boolean]] - } - - @inline - private def safeHasOwnProperty(dict: js.Dictionary[_], key: String): scala.Boolean = - Cache.safeHasOwnProperty(dict, key) - - @js.native - private trait DictionaryRawApply[A] extends js.Object { - /** Reads a field of this object by its name. - * - * This must not be called if the dictionary does not contain the key. - */ - @JSBracketAccess - def rawApply(key: String): A = js.native - - /** Writes a field of this object. */ - @JSBracketAccess - def rawUpdate(key: String, value: A): Unit = js.native - } - - @inline - def dictEmpty[A](): js.Dictionary[A] = - new js.Object().asInstanceOf[js.Dictionary[A]] - - @inline - def dictGetOrElse[A](dict: js.Dictionary[_ <: A], key: String)( - default: => A): A = { - if (dictContains(dict, key)) - dictRawApply(dict, key) - else - default - } - - def dictGetOrElseAndRemove[A](dict: js.Dictionary[_ <: A], key: String, - default: A): A = { - if (dictContains(dict, key)) { - val result = dictRawApply(dict, key) - js.special.delete(dict, key) - result - } else { - default - } - } - - @inline - def dictRawApply[A](dict: js.Dictionary[A], key: String): A = - dict.asInstanceOf[DictionaryRawApply[A]].rawApply(key) - - def dictContains[A](dict: js.Dictionary[A], key: String): scala.Boolean = { - /* We have to use a safe version of hasOwnProperty, because - * "hasOwnProperty" could be a key of this dictionary. - */ - safeHasOwnProperty(dict, key) - } - - @inline - def dictSet[A](dict: js.Dictionary[A], key: String, value: A): Unit = - dict.asInstanceOf[DictionaryRawApply[A]].rawUpdate(key, value) - - @inline - def forArrayElems[A](array: js.Array[A])(f: A => Any): Unit = { - val len = array.length - var i = 0 - while (i != len) { - f(array(i)) - i += 1 - } - } - - @inline - def arrayRemove[A](array: js.Array[A], index: Int): Unit = - array.splice(index, 1) - - @inline - def arrayRemoveAndGet[A](array: js.Array[A], index: Int): A = - array.splice(index, 1)(0) - - @inline - def arrayExists[A](array: js.Array[A])(f: A => Boolean): Boolean = { - // scalastyle:off return - val len = array.length - var i = 0 - while (i != len) { - if (f(array(i))) - return true - i += 1 - } - false - // scalastyle:on return - } - - @js.native - private trait RawMap[K, V] extends js.Object { - def has(key: K): Boolean = js.native - def keys(): js.Iterator[K] = js.native - def set(key: K, value: V): js.Map[K, V] = js.native - def get(key: K): V = js.native - } - - @inline def mapHas[K, V](m: js.Map[K, V], key: K): Boolean = - m.asInstanceOf[RawMap[K, V]].has(key) - - @inline def mapGet[K, V](m: js.Map[K, V], key: K): V = - m.asInstanceOf[RawMap[K, V]].get(key) - - @inline def mapSet[K, V](m: js.Map[K, V], key: K, value: V): Unit = - m.asInstanceOf[RawMap[K, V]].set(key, value) - - @inline def mapGetOrElse[K, V](m: js.Map[K, V], key: K)(default: => V): V = - if (mapHas(m, key)) mapGet(m, key) - else default - - @inline def mapGetOrElseUpdate[K, V](m: js.Map[K, V], key: K)(default: => V): V = { - if (mapHas(m, key)) { - mapGet(m, key) - } else { - val value = default - mapSet(m, key, value) - value - } - } -} diff --git a/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala b/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala index 994c2acb6d..fb8cb030a5 100644 --- a/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala +++ b/javalib/src/main/scala/java/util/concurrent/CopyOnWriteArrayList.scala @@ -13,10 +13,10 @@ package java.util.concurrent import java.lang.Cloneable +import java.lang.Utils._ import java.lang.{reflect => jlr} import java.util._ import java.util.function.{Predicate, UnaryOperator} -import java.util.JSUtils._ import scala.annotation.tailrec diff --git a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala index 257e399807..e493d51fc2 100644 --- a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala +++ b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala @@ -14,7 +14,7 @@ package java.util.regex import scala.annotation.{tailrec, switch} -import java.util.JSUtils._ +import java.lang.Utils._ import scala.scalajs.js diff --git a/javalib/src/main/scala/java/util/regex/Matcher.scala b/javalib/src/main/scala/java/util/regex/Matcher.scala index 6385dbd96a..07f94a8076 100644 --- a/javalib/src/main/scala/java/util/regex/Matcher.scala +++ b/javalib/src/main/scala/java/util/regex/Matcher.scala @@ -12,7 +12,7 @@ package java.util.regex -import java.util.JSUtils._ +import java.lang.Utils._ import scala.annotation.switch diff --git a/javalib/src/main/scala/java/util/regex/Pattern.scala b/javalib/src/main/scala/java/util/regex/Pattern.scala index a26bff33d0..05ee30db58 100644 --- a/javalib/src/main/scala/java/util/regex/Pattern.scala +++ b/javalib/src/main/scala/java/util/regex/Pattern.scala @@ -14,7 +14,7 @@ package java.util.regex import scala.annotation.tailrec -import java.util.JSUtils._ +import java.lang.Utils._ import java.util.ScalaOps._ import scala.scalajs.js diff --git a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala index 5011bab65a..51253c5744 100644 --- a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala +++ b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala @@ -25,7 +25,7 @@ import java.lang.Character.{ MAX_LOW_SURROGATE } -import java.util.JSUtils._ +import java.lang.Utils._ import java.util.ScalaOps._ import scala.scalajs.js diff --git a/project/Build.scala b/project/Build.scala index 320b8eb440..f058e29137 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1728,8 +1728,8 @@ object Build { case Default2_13ScalaVersion => Some(ExpectedSizes( - fastLink = 728000 to 729000, - fullLink = 156000 to 157000, + fastLink = 727000 to 728000, + fullLink = 155000 to 156000, fastLinkGz = 91000 to 92000, fullLinkGz = 40000 to 41000, )) From ddf27965579004349f1b5509406ae768fc9ef574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 23 Jul 2022 18:19:46 +0200 Subject: [PATCH 36/75] Refactor: Rename Serializers.Hacks.use1X to useX. As the IR version is now 1.11, it gets confusing that `use11` is about 1.1 and not 1.11. The major version number is irrelevant anyway, since a new major version would break binary compatibility and all hacks would be removed. --- .../scala/org/scalajs/ir/Serializers.scala | 52 +++++++++---------- project/BinaryIncompatibilities.scala | 2 + 2 files changed, 28 insertions(+), 26 deletions(-) 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 ef2bc10ab0..78f925eb73 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -1067,7 +1067,7 @@ object Serializers { case TagAssign => val lhs0 = readTree() - val lhs = if (hacks.use14 && lhs0.tpe == NothingType) { + val lhs = if (hacks.use4 && lhs0.tpe == NothingType) { /* Note [Nothing FieldDef rewrite] * (throw qual.field[null]) = rhs --> qual.field[null] = rhs */ @@ -1112,7 +1112,7 @@ object Serializers { val field = readFieldIdent() val tpe = readType() - if (hacks.use14 && tpe == NothingType) { + if (hacks.use4 && tpe == NothingType) { /* Note [Nothing FieldDef rewrite] * qual.field[nothing] --> throw qual.field[null] */ @@ -1221,7 +1221,7 @@ object Serializers { val superClass = readOptClassIdent() val parents = readClassIdents() - /* jsSuperClass is not hacked like in readMemberDef.bodyHack15. The + /* jsSuperClass is not hacked like in readMemberDef.bodyHack5. The * compilers before 1.6 always use a simple VarRef() as jsSuperClass, * when there is one, so no hack is required. */ @@ -1240,8 +1240,8 @@ object Serializers { implicit val pos = readPosition() val tag = readByte() - def bodyHack15(body: Tree, isStat: Boolean): Tree = { - if (!hacks.use15) { + def bodyHack5(body: Tree, isStat: Boolean): Tree = { + if (!hacks.use5) { body } else { /* #4442 and #4601: Patch Labeled, If, Match and TryCatch nodes in @@ -1274,7 +1274,7 @@ object Serializers { } } - def bodyHack15Expr(body: Tree): Tree = bodyHack15(body, isStat = false) + def bodyHack5Expr(body: Tree): Tree = bodyHack5(body, isStat = false) (tag: @switch) match { case TagFieldDef => @@ -1283,7 +1283,7 @@ object Serializers { val originalName = readOriginalName() val ftpe0 = readType() - val ftpe = if (hacks.use14 && ftpe0 == NothingType) { + val ftpe = if (hacks.use4 && ftpe0 == NothingType) { /* Note [Nothing FieldDef rewrite] * val field: nothing --> val field: null */ @@ -1315,7 +1315,7 @@ object Serializers { * rewrite it as a static initializers instead (``). */ val name0 = readMethodIdent() - if (hacks.use11 && + if (hacks.use1 && name0.name == ClassInitializerName && !ownerKind.isJSType) { MethodIdent(StaticInitializerName)(name0.pos) @@ -1330,7 +1330,7 @@ object Serializers { val body = readOptTree() val optimizerHints = OptimizerHints.fromBits(readInt()) - if (hacks.use10 && + if (hacks.use0 && flags.namespace == MemberNamespace.Public && owner == HackNames.SystemModule && name.name == HackNames.identityHashCodeName) { @@ -1344,7 +1344,7 @@ object Serializers { MethodDef(flags, name, originalName, args, resultType, patchedBody)( patchedOptimizerHints, optHash) - } else if (hacks.use14 && + } else if (hacks.use4 && flags.namespace == MemberNamespace.Public && owner == ObjectClass && name.name == HackNames.cloneName) { @@ -1375,7 +1375,7 @@ object Serializers { MethodDef(flags, name, originalName, args, resultType, patchedBody)( patchedOptimizerHints, optHash) } else { - val patchedBody = body.map(bodyHack15(_, isStat = resultType == NoType)) + val patchedBody = body.map(bodyHack5(_, isStat = resultType == NoType)) MethodDef(flags, name, originalName, args, resultType, patchedBody)( optimizerHints, optHash) } @@ -1388,19 +1388,19 @@ object Serializers { assert(len >= 0) val flags = MemberFlags.fromBits(readInt()) - val name = bodyHack15Expr(readTree()) + val name = bodyHack5Expr(readTree()) val (params, restParam) = readParamDefsWithRest() - val body = bodyHack15Expr(readTree()) + val body = bodyHack5Expr(readTree()) JSMethodDef(flags, name, params, restParam, body)( OptimizerHints.fromBits(readInt()), optHash) case TagJSPropertyDef => val flags = MemberFlags.fromBits(readInt()) - val name = bodyHack15Expr(readTree()) - val getterBody = readOptTree().map(bodyHack15Expr(_)) + val name = bodyHack5Expr(readTree()) + val getterBody = readOptTree().map(bodyHack5Expr(_)) val setterArgAndBody = { if (readBoolean()) - Some((readParamDef(), bodyHack15Expr(readTree()))) + Some((readParamDef(), bodyHack5Expr(readTree()))) else None } @@ -1419,7 +1419,7 @@ object Serializers { // #4409: Filter out abstract methods in non-native JS classes for version < 1.5 if (ownerKind.isJSClass) { - if (hacks.use14) { + if (hacks.use4) { memberDefs.filter { m => m match { case MethodDef(_, _, _, _, _, None) => false @@ -1457,7 +1457,7 @@ object Serializers { readMemberDef(owner, ownerKind).asInstanceOf[JSMethodDef] def readModuleID(): String = - if (hacks.use12) DefaultModuleID + if (hacks.use2) DefaultModuleID else readString() (tag: @switch) match { @@ -1513,7 +1513,7 @@ object Serializers { val ptpe = readType() val mutable = readBoolean() - if (hacks.use14) { + if (hacks.use4) { val rest = readBoolean() assert(!rest, "Illegal rest parameter") } @@ -1525,7 +1525,7 @@ object Serializers { List.fill(readInt())(readParamDef()) def readParamDefsWithRest(): (List[ParamDef], Option[ParamDef]) = { - if (hacks.use14) { + if (hacks.use4) { val (params, isRest) = List.fill(readInt()) { implicit val pos = readPosition() (ParamDef(readLocalIdent(), readOriginalName(), readType(), readBoolean()), readBoolean()) @@ -1808,17 +1808,17 @@ object Serializers { /** Hacks for backwards compatible deserializing. */ private final class Hacks(sourceVersion: String) { - val use10: Boolean = sourceVersion == "1.0" + val use0: Boolean = sourceVersion == "1.0" - val use11: Boolean = use10 || sourceVersion == "1.1" + val use1: Boolean = use0 || sourceVersion == "1.1" - val use12: Boolean = use11 || sourceVersion == "1.2" + val use2: Boolean = use1 || sourceVersion == "1.2" - private val use13: Boolean = use12 || sourceVersion == "1.3" + private val use3: Boolean = use2 || sourceVersion == "1.3" - val use14: Boolean = use13 || sourceVersion == "1.4" + val use4: Boolean = use3 || sourceVersion == "1.4" - val use15: Boolean = use14 || sourceVersion == "1.5" + val use5: Boolean = use4 || sourceVersion == "1.5" } /** Names needed for hacks. */ diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 4713fe6bf8..222c4317f8 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -5,6 +5,8 @@ import com.typesafe.tools.mima.core.ProblemFilters._ object BinaryIncompatibilities { val IR = Seq( + // private, not an issue + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Serializers#Hacks.*"), ) val Linker = Seq( From d6fe1a868b7be50b65ef56380d4ecbdf20a7ce82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 24 Jul 2022 11:35:23 +0200 Subject: [PATCH 37/75] Add JSConstructorDef to replace JSMethodDef(StrLit("constructor")). Previously, the unique JS constructor of a JS class (or module class) was represented in the IR as a `JSMethodDef` whose `name` was a constant `StringLiterl("constructor")`. We had done that because it superficially looks similar to how the constructor is actually defined in JavaScript source code. However, JS constructors are treated in very distinct ways by most of the linker: * they are singled out to be compiled as the class definition, and * their body structure is taken apart to deal with the `JSSuperConstructorCall` and its effects on the environment. This effectively meant that the linker was treating JS constructor defs as a separate data type with a separate structure already, but in an inconvenient and unsafe way. Moreover, it fundamentally prevents to declare a *regular* method called `constructor` in a JS class, at the IR level. Despite what one might think, it is actually possible to declare such a method in a JavaScript class. The two following examples define actual constructors: constructor(...) { ... } "constructor"(...) { ... } but a third variant defines a regular method called `constructor`: ["constructor"](...) { ... } Therefore, it does not make sense for the IR to have a built-in limitation for `JSMethodDef`s named `"constructor"`. --- In this commit, we introduce a separate `MemberDef` called `JSConstructorDef`. Its `body` is structured as a `JSConstructorBody`, which explicitly separates the statements coming before and after the super constructor call. This change simplifies all parts of the linker dealing with JS constructors, and makes them safer in the process. We use a deserialization hack to convert a `JSMethodDef` with a `StringLiteral("constructor")` name into a `JSConstructorDef`. In this commit, we do not change the compiler back-end yet, to test the deserialization hack. --- .../org/scalajs/nscplugin/GenJSCode.scala | 6 ++ .../main/scala/org/scalajs/ir/Hashers.scala | 29 ++++++- .../main/scala/org/scalajs/ir/Printers.scala | 24 +++-- .../scala/org/scalajs/ir/Serializers.scala | 87 ++++++++++++++++++- .../src/main/scala/org/scalajs/ir/Tags.scala | 4 + .../scala/org/scalajs/ir/Transformers.scala | 18 ++++ .../scala/org/scalajs/ir/Traversers.scala | 3 + .../src/main/scala/org/scalajs/ir/Trees.scala | 16 +++- .../scala/org/scalajs/ir/PrintersTest.scala | 43 ++++++++- .../org/scalajs/linker/analyzer/Infos.scala | 15 ++++ .../linker/backend/emitter/ClassEmitter.scala | 13 +-- .../backend/emitter/FunctionEmitter.scala | 12 +++ .../linker/checker/ClassDefChecker.scala | 75 +++++++--------- .../scalajs/linker/checker/IRChecker.scala | 61 ++++++------- .../scalajs/linker/frontend/BaseLinker.scala | 10 +++ .../org/scalajs/linker/frontend/Refiner.scala | 15 +++- .../scalajs/linker/standard/LinkedClass.scala | 3 + .../org/scalajs/linker/AnalyzerTest.scala | 5 +- .../org/scalajs/linker/IRCheckerTest.scala | 17 ++-- .../linker/testutils/TestIRBuilder.scala | 9 ++ project/BinaryIncompatibilities.scala | 2 + 21 files changed, 355 insertions(+), 112 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 3ad1ccabec..78e6ac191b 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -817,6 +817,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) "Non-static, unexported method in non-native JS class") classDefMembers += mdef + case cdef: js.JSConstructorDef => + abort(s"we do not generate JSConstructorDef's yet, at ${cdef.pos}") + case mdef: js.JSMethodDef => mdef.name match { case js.StringLiteral("constructor") => @@ -895,6 +898,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case mdef: js.MethodDef => throw new AssertionError("unexpected MethodDef") + case cdef: js.JSConstructorDef => + throw new AssertionError("unexpected JSConstructorDef") + case mdef: js.JSMethodDef => implicit val pos = mdef.pos val impl = memberLambda(mdef.args, mdef.restParam, mdef.body) 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 4f7ad70c1f..cba76e8b64 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -44,6 +44,28 @@ object Hashers { } } + def hashJSConstructorDef(ctorDef: JSConstructorDef): JSConstructorDef = { + if (ctorDef.hash.isDefined) { + ctorDef + } else { + val hasher = new TreeHasher() + val JSConstructorDef(flags, params, restParam, body) = ctorDef + + hasher.mixPos(ctorDef.pos) + hasher.mixInt(MemberFlags.toBits(flags)) + hasher.mixParamDefs(params) + restParam.foreach(hasher.mixParamDef(_)) + hasher.mixPos(body.pos) + hasher.mixTrees(body.allStats) + hasher.mixInt(OptimizerHints.toBits(ctorDef.optimizerHints)) + + val hash = hasher.finalizeHash() + + JSConstructorDef(flags, params, restParam, body)( + ctorDef.optimizerHints, Some(hash))(ctorDef.pos) + } + } + def hashJSMethodDef(methodDef: JSMethodDef): JSMethodDef = { if (methodDef.hash.isDefined) methodDef else { @@ -67,9 +89,10 @@ object Hashers { /** Hash definitions from a ClassDef where applicable */ def hashMemberDefs(memberDefs: List[MemberDef]): List[MemberDef] = memberDefs.map { - case methodDef: MethodDef => hashMethodDef(methodDef) - case methodDef: JSMethodDef => hashJSMethodDef(methodDef) - case otherDef => otherDef + case methodDef: MethodDef => hashMethodDef(methodDef) + case ctorDef: JSConstructorDef => hashJSConstructorDef(ctorDef) + case methodDef: JSMethodDef => hashJSMethodDef(methodDef) + case otherDef => otherDef } /** Hash the definitions in a ClassDef (where applicable) */ 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 4e3b0b6287..e5cfe2e510 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -78,17 +78,16 @@ object Printers { } protected def printBlock(tree: Tree): Unit = { - tree match { - case Block(trees) => - printColumn(trees, "{", ";", "}") - - case _ => - print('{'); indent(); println() - print(tree) - undent(); println(); print('}') + val trees = tree match { + case Block(trees) => trees + case _ => tree :: Nil } + printBlock(trees) } + protected def printBlock(trees: List[Tree]): Unit = + printColumn(trees, "{", ";", "}") + protected def printSig(args: List[ParamDef], restParam: Option[ParamDef], resultType: Type): Unit = { print("(") @@ -132,6 +131,7 @@ object Printers { case node: JSSpread => print(node) case node: ClassDef => print(node) case node: MemberDef => print(node) + case node: JSConstructorBody => printBlock(node.allStats) case node: TopLevelExportDef => print(node) } } @@ -981,6 +981,14 @@ object Printers { printBlock(body) } + case tree: JSConstructorDef => + val JSConstructorDef(flags, args, restParam, body) = tree + print(tree.optimizerHints) + print(flags.namespace.prefixString) + print("def constructor") + printSig(args, restParam, AnyType) + printBlock(body.allStats) + case tree: JSMethodDef => val JSMethodDef(flags, name, args, restParam, body) = tree print(tree.optimizerHints) 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 78f925eb73..93233992bb 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -667,6 +667,30 @@ object Serializers { writeInt(length) bufferUnderlying.continue() + case ctorDef: JSConstructorDef => + val JSConstructorDef(flags, args, restParam, body) = ctorDef + + writeByte(TagJSConstructorDef) + writeOptHash(ctorDef.hash) + + // Prepare for back-jump and write dummy length + bufferUnderlying.markJump() + writeInt(-1) + + // Write out ctor def + writeInt(MemberFlags.toBits(flags)) + writeParamDefs(args); writeOptParamDef(restParam) + writePosition(body.pos) + writeTrees(body.beforeSuper) + writeTree(body.superCall) + writeTrees(body.afterSuper) + writeInt(OptimizerHints.toBits(ctorDef.optimizerHints)) + + // Jump back and write true length + val length = bufferUnderlying.jumpBack() + writeInt(length) + bufferUnderlying.continue() + case methodDef: JSMethodDef => val JSMethodDef(flags, name, args, restParam, body) = methodDef @@ -1228,14 +1252,50 @@ object Serializers { val jsSuperClass = readOptTree() val jsNativeLoadSpec = readJSNativeLoadSpec() - val memberDefs = readMemberDefs(name.name, kind) + val memberDefs0 = readMemberDefs(name.name, kind) val topLevelExportDefs = readTopLevelExportDefs(name.name, kind) val optimizerHints = OptimizerHints.fromBits(readInt()) + + val memberDefs = + if (/*hacks.use8 &&*/ kind.isJSClass) memberDefs0.map(jsConstructorDefHack(_)) // scalastyle:ignore + else memberDefs0 + ClassDef(name, originalName, kind, jsClassCaptures, superClass, parents, jsSuperClass, jsNativeLoadSpec, memberDefs, topLevelExportDefs)( optimizerHints) } + private def jsConstructorDefHack(memberDef: MemberDef): MemberDef = { + memberDef match { + case methodDef @ JSMethodDef(flags, StringLiteral("constructor"), args, restParam, body) + if flags.namespace == MemberNamespace.Public => + val bodyStats = body match { + case Block(stats) => stats + case _ => body :: Nil + } + + bodyStats.span(!_.isInstanceOf[JSSuperConstructorCall]) match { + case (beforeSuper, (superCall: JSSuperConstructorCall) :: afterSuper) => + val newFlags = flags.withNamespace(MemberNamespace.Constructor) + val newBody = JSConstructorBody(beforeSuper, superCall, afterSuper)(body.pos) + val ctorDef = JSConstructorDef(newFlags, args, restParam, newBody)( + methodDef.optimizerHints, None)(methodDef.pos) + Hashers.hashJSConstructorDef(ctorDef) + + case _ => + /* This is awkward: we have an old-style JS constructor that is + * structurally invalid. We crash in order not to silently + * ignore errors. + */ + throw new IOException( + s"Found invalid pre-1.11 JS constructor def at ${methodDef.pos}:\n${methodDef.show}") + } + + case _ => + memberDef + } + } + def readMemberDef(owner: ClassName, ownerKind: ClassKind): MemberDef = { implicit val pos = readPosition() val tag = readByte() @@ -1380,6 +1440,25 @@ object Serializers { optimizerHints, optHash) } + case TagJSConstructorDef => + 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. + */ + + val flags = MemberFlags.fromBits(readInt()) + val (params, restParam) = readParamDefsWithRest() + val bodyPos = readPosition() + val beforeSuper = readTrees() + val superCall = readTree().asInstanceOf[JSSuperConstructorCall] + val afterSuper = readTrees() + val body = JSConstructorBody(beforeSuper, superCall, afterSuper)(bodyPos) + JSConstructorDef(flags, params, restParam, body)( + OptimizerHints.fromBits(readInt()), optHash) case TagJSMethodDef => val optHash = readOptHash() @@ -1819,6 +1898,12 @@ object Serializers { 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" } /** Names needed for hacks. */ 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 b4efac66d9..a084304571 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Tags.scala @@ -139,6 +139,10 @@ private[ir] object Tags { final val TagJSNativeMemberDef = TagJSPropertyDef + 1 + // New in 1.11 + + final val TagJSConstructorDef = TagJSNativeMemberDef + 1 + // Tags for top-level export defs final val TagTopLevelJSClassExportDef = 1 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 f6629f98ed..69b58dbe22 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -253,6 +253,11 @@ object Transformers { MethodDef(flags, name, originalName, args, resultType, newBody)( memberDef.optimizerHints, None) + case ctorDef: JSConstructorDef => + val JSConstructorDef(flags, args, restParam, body) = memberDef + JSConstructorDef(flags, args, restParam, transformJSConstructorBody(body))( + ctorDef.optimizerHints, None) + case memberDef: JSMethodDef => val JSMethodDef(flags, name, args, restParam, body) = memberDef JSMethodDef(flags, name, args, restParam, transformExpr(body))( @@ -269,6 +274,19 @@ object Transformers { } } + 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 + } + + JSConstructorBody(newBeforeSuper, newSuperCall, newAfterSuper) + } + def transformTopLevelExportDef( exportDef: TopLevelExportDef): TopLevelExportDef = { 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 8370736afb..d202f6a4f2 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -244,6 +244,9 @@ object Traversers { case MethodDef(_, _, _, _, _, body) => body.foreach(traverse) + case JSConstructorDef(_, _, _, body) => + body.allStats.foreach(traverse) + case JSMethodDef(_, _, _, _, body) => traverse(body) 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 a8b457be3d..c3566a9b2e 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -1127,7 +1127,8 @@ object Trees { /** Any member of a `ClassDef`. * - * Partitioned into `AnyFieldDef`, `MethodDef` and `JSMethodPropDef`. + * Partitioned into `AnyFieldDef`, `MethodDef`, `JSConstructorDef` and + * `JSMethodPropDef`. */ sealed abstract class MemberDef extends IRNode { val flags: MemberFlags @@ -1153,6 +1154,19 @@ object Trees { def methodName: MethodName = name.name } + sealed case class JSConstructorDef(flags: MemberFlags, + args: List[ParamDef], restParam: Option[ParamDef], body: JSConstructorBody)( + val optimizerHints: OptimizerHints, val hash: Option[TreeHash])( + implicit val pos: Position) + extends MemberDef + + sealed case class JSConstructorBody( + beforeSuper: List[Tree], superCall: JSSuperConstructorCall, afterSuper: List[Tree])( + implicit val pos: Position) + extends IRNode { + val allStats: List[Tree] = beforeSuper ::: superCall :: afterSuper + } + sealed abstract class JSMethodPropDef extends MemberDef sealed case class JSMethodDef(flags: MemberFlags, name: Tree, 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 bc90390910..13972db5ed 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -26,7 +26,7 @@ import Types._ import TestIRBuilder._ class PrintersTest { - import MemberNamespace.{Private, PublicStatic => Static, PrivateStatic} + import MemberNamespace.{Constructor, Private, PublicStatic => Static, PrivateStatic} /** An original name. */ private val TestON = OriginalName("orig name") @@ -1253,6 +1253,47 @@ class PrintersTest { IntType, None)(NoOptHints, None)) } + @Test def printJSConstructorDef(): Unit = { + assertPrintEquals( + """ + |constructor def constructor(x: any): any = { + | 5; + | super(6); + | (void 0) + |} + """, + JSConstructorDef(MemberFlags.empty.withNamespace(Constructor), + List(ParamDef("x", NON, AnyType, mutable = false)), None, + JSConstructorBody(List(i(5)), JSSuperConstructorCall(List(i(6))), List(Undefined())))( + NoOptHints, None)) + + assertPrintEquals( + """ + |constructor def constructor(x: any, ...y: any): any = { + | super(6); + | 7 + |} + """, + JSConstructorDef(MemberFlags.empty.withNamespace(Constructor), + List(ParamDef("x", NON, AnyType, mutable = false)), + Some(ParamDef("y", NON, AnyType, mutable = false)), + JSConstructorBody(Nil, JSSuperConstructorCall(List(i(6))), List(i(7))))( + NoOptHints, None)) + + // This example is an invalid constructor, but it should be printed anyway + assertPrintEquals( + """ + |def constructor(x{orig name}: any): any = { + | 5; + | super(6) + |} + """, + JSConstructorDef(MemberFlags.empty, + List(ParamDef("x", TestON, AnyType, mutable = false)), None, + JSConstructorBody(List(i(5)), JSSuperConstructorCall(List(i(6))), Nil))( + NoOptHints, None)) + } + @Test def printJSMethodDef(): Unit = { assertPrintEquals( """ diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala index 8fd9b501c8..6bc7eb807f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala @@ -376,6 +376,9 @@ object Infos { case methodDef: MethodDef => builder.addMethod(generateMethodInfo(methodDef)) + case ctorDef: JSConstructorDef => + builder.addExportedMember(generateJSConstructorInfo(ctorDef)) + case methodDef: JSMethodDef => builder.addExportedMember(generateJSMethodInfo(methodDef)) @@ -410,6 +413,12 @@ object Infos { def generateMethodInfo(methodDef: MethodDef): MethodInfo = new GenInfoTraverser().generateMethodInfo(methodDef) + /** Generates the [[ReachabilityInfo]] of a + * [[org.scalajs.ir.Trees.JSConstructorDef Trees.JSConstructorDef]]. + */ + def generateJSConstructorInfo(ctorDef: JSConstructorDef): ReachabilityInfo = + new GenInfoTraverser().generateJSConstructorInfo(ctorDef) + /** Generates the [[ReachabilityInfo]] of a * [[org.scalajs.ir.Trees.JSMethodDef Trees.JSMethodDef]]. */ @@ -452,6 +461,12 @@ object Infos { ) } + def generateJSConstructorInfo(ctorDef: JSConstructorDef): ReachabilityInfo = { + ctorDef.body.allStats.foreach(traverse(_)) + + builder.result() + } + def generateJSMethodInfo(methodDef: JSMethodDef): ReachabilityInfo = { traverse(methodDef.name) traverse(methodDef.body) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala index fc6d6a006f..8e42ee7365 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala @@ -382,14 +382,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { require(tree.kind.isJSClass) - tree.exportedMembers.map(_.value) collectFirst { - case JSMethodDef(flags, StringLiteral("constructor"), params, restParam, body) - if flags.namespace == MemberNamespace.Public => - desugarToFunction(tree.className, params, restParam, body, resultType = AnyType) - } getOrElse { + val JSConstructorDef(_, params, restParam, body) = tree.jsConstructorDef.getOrElse { throw new IllegalArgumentException( s"${tree.className} does not have an exported constructor") - } + }.value + + desugarToFunction(tree.className, params, restParam, body) } /** Generates the creation of fields for a Scala class. */ @@ -1065,9 +1063,6 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { val exportsWithGlobals = tree.exportedMembers map { member => member.value match { - case JSMethodDef(flags, StringLiteral("constructor"), _, _, _) - if flags.namespace == MemberNamespace.Public && tree.kind.isJSClass => - WithGlobals(js.Skip()(member.value.pos)) case m: JSMethodDef => genJSMethod(tree, useESClass, m) case p: JSPropertyDef => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index d7bfb80296..465534634d 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -264,6 +264,18 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { resultType) } + /** Desugars parameters and body to a JS function (JS constructor variant). + */ + def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef], + restParam: Option[ParamDef], body: JSConstructorBody)( + implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, + pos: Position): WithGlobals[js.Function] = { + val bodyBlock = Block(body.allStats)(body.pos) + new JSDesugar().desugarToFunction(params, restParam, bodyBlock, + isStat = false, + Env.empty(AnyType).withEnclosingClassName(Some(enclosingClassName))) + } + /** Desugars parameters and body to a JS function. */ def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef], diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index 051906569b..f6e4860014 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -80,6 +80,7 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) classDef.memberDefs.foreach { case fieldDef: AnyFieldDef => checkFieldDef(fieldDef) case methodDef: MethodDef => checkMethodDef(methodDef) + case jsCtorDef: JSConstructorDef => checkJSConstructorDef(jsCtorDef) case jsMethodDef: JSMethodDef => checkJSMethodDef(jsMethodDef) case jsPropertyDef: JSPropertyDef => checkJSPropertyDef(jsPropertyDef) case jsNativeMemberDef: JSNativeMemberDef => checkJSNativeMemberDef(jsNativeMemberDef) @@ -93,6 +94,9 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) if (classDef.kind == ClassKind.ModuleClass && methods(MemberNamespace.Constructor.ordinal).size != 1) reportError("Module class must have exactly 1 constructor") + + if (classDef.kind.isJSClass && classDef.memberDefs.count(_.isInstanceOf[JSConstructorDef]) != 1) + reportError("JS classes and module classes must have exactly 1 constructor") } private def checkKind()(implicit ctx: ErrorContext): Unit = { @@ -256,6 +260,32 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) body.foreach(checkTree(_, Env.fromParams(params))) } + private def checkJSConstructorDef(ctorDef: JSConstructorDef): Unit = withPerMethodState { + val JSConstructorDef(flags, params, restParam, body) = ctorDef + implicit val ctx = ErrorContext(ctorDef) + + if (flags.isMutable) + reportError("A JS constructor cannot have the flag Mutable") + if (flags.namespace != MemberNamespace.Constructor) + reportError("A JS constructor must be in the constructor namespace") + + if (!classDef.kind.isJSClass) + reportError("JS constructor defs can only appear in JS classes") + + checkJSParamDefs(params, restParam) + + val startEnv = Env.fromParams(classDef.jsClassCaptures.getOrElse(Nil) ++ params ++ restParam) + .withHasNewTarget(true) + + val envJustBeforeSuper = body.beforeSuper.foldLeft(startEnv) { (prevEnv, stat) => + checkTree(stat, prevEnv) + } + checkTreeOrSpreads(body.superCall.args, envJustBeforeSuper) + body.afterSuper.foldLeft(envJustBeforeSuper) { (prevEnv, stat) => + checkTree(stat, prevEnv) + } + } + private def checkJSMethodDef(methodDef: JSMethodDef): Unit = withPerMethodState { val JSMethodDef(flags, pName, params, restParam, body) = methodDef implicit val ctx = ErrorContext(methodDef) @@ -277,53 +307,10 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) checkExportedPropertyName(pName) checkJSParamDefs(params, restParam) - val isJSConstructor = { - classDef.kind.isJSClass && !static && { - pName match { - case StringLiteral("constructor") => true - case _ => false - } - } - } - val env = Env.fromParams(classDef.jsClassCaptures.getOrElse(Nil) ++ params ++ restParam) - if (isJSConstructor) - checkJSClassConstructorBody(body, env.withHasNewTarget(true)) - else - checkTree(body, env) - } - - private def checkJSClassConstructorBody(body: Tree, env: Env)( - implicit ctx: ErrorContext): Unit = { - val bodyStats = body match { - case Block(stats) => stats - case _ => body :: Nil - } - - var seenSuperCall = false - var curEnv = env - - bodyStats.foreach { - case tree @ JSSuperConstructorCall(args) => - if (seenSuperCall) { - implicit val ctx = ErrorContext(tree) - reportError("Duplicate JSSuperConstructorCall") - } - - seenSuperCall = true - checkTreeOrSpreads(args, curEnv) - - case tree => - curEnv = checkTree(tree, curEnv) - } - - if (!seenSuperCall) { - reportError( - "A JS class constructor must contain one super constructor " + - "call at the top-level") - } + checkTree(body, env) } private def checkJSPropertyDef(propDef: JSPropertyDef): Unit = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 4ecbbf159a..0d9cdbbac1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -57,6 +57,10 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { checkMethodDef(versioned.value, classDef) } + classDef.jsConstructorDef.foreach { versioned => + checkJSConstructorDef(versioned.value, classDef) + } + classDef.exportedMembers.foreach { versioned => versioned.value match { case jsMethodDef: JSMethodDef => @@ -136,6 +140,25 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { } } + private def checkJSConstructorDef(ctorDef: JSConstructorDef, + clazz: LinkedClass): Unit = { + val JSConstructorDef(flags, params, restParam, body) = ctorDef + implicit val ctx = ErrorContext(ctorDef) + + // JS constructors only get a valid `this` after the super call. + + val beforeSuperEnv = Env.fromSignature(thisType = NoType, inConstructorOf = Some(clazz.name.name)) + body.beforeSuper.foreach(typecheck(_, beforeSuperEnv)) + body.superCall.args.foreach(typecheckExprOrSpread(_, beforeSuperEnv)) + + val afterSuperEnv = beforeSuperEnv.withThis(AnyType) + body.afterSuper.foreach(typecheck(_, afterSuperEnv)) + + val resultType = body.afterSuper.lastOption.fold[Type](NoType)(_.tpe) + if (resultType == NoType) + reportError(i"${AnyType} expected but $resultType found for JS constructor body") + } + private def checkJSMethodDef(methodDef: JSMethodDef, clazz: LinkedClass): Unit = { val JSMethodDef(flags, pName, params, restParam, body) = methodDef @@ -145,27 +168,13 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { typecheckExpr(pName, Env.empty) - val isJSConstructor = { - clazz.kind.isJSClass && !static && { - pName match { - case StringLiteral("constructor") => true - case _ => false - } - } - } - val thisType = { - // JS constructors only get a valid `this` after the super call. - if (static || isJSConstructor) NoType + if (static) NoType else if (clazz.kind.isJSClass) AnyType else ClassType(clazz.name.name) } - val inConstructorOf = - if (isJSConstructor) Some(clazz.name.name) - else None - - val bodyEnv = Env.fromSignature(thisType, inConstructorOf) + val bodyEnv = Env.fromSignature(thisType) typecheckExpect(body, bodyEnv, AnyType) } @@ -240,7 +249,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { case Skip() => case Block(trees) => - typecheckBlockTrees(trees, env) + trees.foreach(typecheck(_, env)) case Labeled(label, tpe, body) => typecheckExpect(body, env.withLabeledReturnType(label.name, tpe), tpe) @@ -604,10 +613,6 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { for (arg <- args) typecheckExprOrSpread(arg, env) - case JSSuperConstructorCall(args) => - for (arg <- args) - typecheckExprOrSpread(arg, env) - case JSImportCall(arg) => typecheckExpr(arg, env) @@ -707,23 +712,11 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { typecheckExpect(value, env, ctpe) } - case _:RecordSelect | _:RecordValue | _:Transient => + case _:RecordSelect | _:RecordValue | _:Transient | _:JSSuperConstructorCall => reportError("invalid tree") } } - private def typecheckBlockTrees(trees: List[Tree], env: Env): Env = { - trees.foldLeft(env) { (prevEnv, tree) => - typecheck(tree, prevEnv) - tree match { - case JSSuperConstructorCall(_) => - prevEnv.withThis(AnyType) - case _ => - prevEnv - } - } - } - private def checkIsAsInstanceTargetType(tpe: Type)( implicit ctx: ErrorContext): Unit = { tpe match { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala index b54771092c..4757dd6080 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala @@ -150,6 +150,7 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { val fields = List.newBuilder[AnyFieldDef] val methods = List.newBuilder[Versioned[MethodDef]] val jsNativeMembers = List.newBuilder[JSNativeMemberDef] + var jsConstructorDef: Option[Versioned[JSConstructorDef]] = None val exportedMembers = List.newBuilder[Versioned[JSMethodPropDef]] def linkedMethod(m: MethodDef) = { @@ -173,6 +174,14 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { methods += linkedMethod(m) } + case m: JSConstructorDef => + if (analyzerInfo.isAnySubclassInstantiated) { + assert(jsConstructorDef.isEmpty, + s"Duplicate JS constructor in ${classDef.name.name} at ${m.pos}") + val version = m.hash.map(Hashers.hashAsVersion(_)) + jsConstructorDef = Some(new Versioned(m, version)) + } + case m: JSMethodDef => if (analyzerInfo.isAnySubclassInstantiated) { val version = m.hash.map(Hashers.hashAsVersion(_)) @@ -206,6 +215,7 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { classDef.jsNativeLoadSpec, fields.result(), methods.result(), + jsConstructorDef, exportedMembers.result(), jsNativeMembers.result(), classDef.optimizerHints, diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala index 45afb92825..7c87e9ecd1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala @@ -176,6 +176,7 @@ private object Refiner { private class LinkedClassInfoCache { private var cacheUsed: Boolean = false private val methodsInfoCaches = LinkedMethodDefsInfosCache() + private val jsConstructorInfoCache = new LinkedJSConstructorDefInfoCache() private val exportedMembersInfoCaches = LinkedJSMethodPropDefsInfosCache() private var info: Infos.ClassInfo = _ @@ -198,6 +199,8 @@ private object Refiner { builder.addMethod(methodsInfoCaches.getInfo(linkedMethod)) for (jsNativeMember <- linkedClass.jsNativeMembers) builder.addJSNativeMember(jsNativeMember) + for (jsConstructorDef <- linkedClass.jsConstructorDef) + builder.addExportedMember(jsConstructorInfoCache.getInfo(jsConstructorDef)) for (info <- exportedMembersInfoCaches.getInfos(linkedClass.exportedMembers)) builder.addExportedMember(info) @@ -212,6 +215,7 @@ private object Refiner { if (result) { // No point in cleaning the inner caches if the whole class disappears methodsInfoCaches.cleanAfterRun() + jsConstructorInfoCache.cleanAfterRun() exportedMembersInfoCaches.cleanAfterRun() } result @@ -314,14 +318,21 @@ private object Refiner { private final class LinkedMethodDefInfoCache extends AbstractLinkedMemberInfoCache[MethodDef, Infos.MethodInfo] { - protected def computeInfo(member: MethodDef): Infos.MethodInfo = + protected def computeInfo(member: MethodDef): Infos.MethodInfo = Infos.generateMethodInfo(member) } + private final class LinkedJSConstructorDefInfoCache + extends AbstractLinkedMemberInfoCache[JSConstructorDef, Infos.ReachabilityInfo] { + + protected def computeInfo(member: JSConstructorDef): Infos.ReachabilityInfo = + Infos.generateJSConstructorInfo(member) + } + private final class LinkedJSMethodPropDefInfoCache extends AbstractLinkedMemberInfoCache[JSMethodPropDef, Infos.ReachabilityInfo] { - protected def computeInfo(member: JSMethodPropDef): Infos.ReachabilityInfo = { + protected def computeInfo(member: JSMethodPropDef): Infos.ReachabilityInfo = { member match { case methodDef: JSMethodDef => Infos.generateJSMethodInfo(methodDef) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala index f2fec452cf..50111a07ec 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala @@ -41,6 +41,7 @@ final class LinkedClass( val jsNativeLoadSpec: Option[JSNativeLoadSpec], val fields: List[AnyFieldDef], val methods: List[Versioned[MethodDef]], + val jsConstructorDef: Option[Versioned[JSConstructorDef]], val exportedMembers: List[Versioned[JSMethodPropDef]], val jsNativeMembers: List[JSNativeMemberDef], val optimizerHints: OptimizerHints, @@ -120,6 +121,7 @@ final class LinkedClass( jsNativeLoadSpec: Option[JSNativeLoadSpec] = this.jsNativeLoadSpec, fields: List[AnyFieldDef] = this.fields, methods: List[Versioned[MethodDef]] = this.methods, + jsConstructorDef: Option[Versioned[JSConstructorDef]] = this.jsConstructorDef, exportedMembers: List[Versioned[JSMethodPropDef]] = this.exportedMembers, jsNativeMembers: List[JSNativeMemberDef] = this.jsNativeMembers, optimizerHints: OptimizerHints = this.optimizerHints, @@ -142,6 +144,7 @@ final class LinkedClass( jsNativeLoadSpec, fields, methods, + jsConstructorDef, exportedMembers, jsNativeMembers, optimizerHints, diff --git a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala index 702214b9e5..79736326b7 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala @@ -532,9 +532,10 @@ class AnalyzerTest { kind = ClassKind.JSClass, superClass = Some(JSObjectLikeClass), memberDefs = List( - JSMethodDef(EMF, str("constructor"), Nil, None, Block( + JSConstructorDef(JSCtorFlags, Nil, None, JSConstructorBody( + Nil, JSSuperConstructorCall(Nil), - JSNewTarget() + JSNewTarget() :: Nil ))(EOH, None) ) ), diff --git a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala index 988a5ba63e..872644f9f0 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala @@ -97,7 +97,7 @@ class IRCheckerTest { classDef("B", kind = ClassKind.NativeJSClass, superClass = Some(ObjectClass)), classDef("C", kind = ClassKind.NativeJSModuleClass, superClass = Some(ObjectClass)), - classDef("D", kind = ClassKind.JSClass, superClass = Some("A")), + classDef("D", kind = ClassKind.JSClass, superClass = Some("A"), memberDefs = List(trivialJSCtor)), mainTestClassDef(Block( LoadJSConstructor("B"), @@ -126,8 +126,10 @@ class IRCheckerTest { kind = ClassKind.JSClass, superClass = Some(JSObjectLikeClass), memberDefs = List( - JSMethodDef(EMF, str("constructor"), Nil, None, Block( - JSSuperConstructorCall(Nil) + JSConstructorDef(JSCtorFlags, Nil, None, JSConstructorBody( + Nil, + JSSuperConstructorCall(Nil), + Nil ))(EOH, None) ) ), @@ -139,7 +141,7 @@ class IRCheckerTest { for (log <- testLinkIRErrors(classDefs, MainTestModuleInitializers)) yield { log.assertContainsError( - "any expected but found for tree of type org.scalajs.ir.Trees$JSSuperConstructorCall") + "any expected but found for JS constructor body") } } @@ -153,9 +155,10 @@ class IRCheckerTest { kind = ClassKind.JSClass, superClass = Some(JSObjectLikeClass), memberDefs = List( - JSMethodDef(EMF, str("constructor"), Nil, None, Block( + JSConstructorDef(JSCtorFlags, Nil, None, JSConstructorBody( + Nil, JSSuperConstructorCall(Nil), - VarDef("x", NON, IntType, mutable = false, int(5)) + VarDef("x", NON, IntType, mutable = false, int(5)) :: Nil ))(EOH, None) ) ), @@ -167,7 +170,7 @@ class IRCheckerTest { for (log <- testLinkIRErrors(classDefs, MainTestModuleInitializers)) yield { log.assertContainsError( - "any expected but found for tree of type org.scalajs.ir.Trees$Block") + "any expected but found for JS constructor body") } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala index 03bdad9d5d..4fae324104 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala @@ -33,6 +33,8 @@ object TestIRBuilder { val EOH = OptimizerHints.empty val NON = NoOriginalName + val JSCtorFlags = EMF.withNamespace(MemberNamespace.Constructor) + val V = VoidRef val I = IntRef val Z = BooleanRef @@ -88,6 +90,12 @@ object TestIRBuilder { EOH, None) } + def trivialJSCtor: JSConstructorDef = { + JSConstructorDef(JSCtorFlags, Nil, None, + JSConstructorBody(Nil, JSSuperConstructorCall(Nil), Undefined() :: Nil))( + EOH, None) + } + val MainMethodName: MethodName = m("main", List(AT), VoidRef) def mainMethodDef(body: Tree): MethodDef = { @@ -128,6 +136,7 @@ object TestIRBuilder { def requiredMemberDefs(className: ClassName, classKind: ClassKind): List[MemberDef] = { if (classKind == ClassKind.ModuleClass) List(trivialCtor(className)) + else if (classKind.isJSClass) List(trivialJSCtor) else Nil } diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 222c4317f8..7f3b4db846 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -10,6 +10,8 @@ object BinaryIncompatibilities { ) val Linker = Seq( + // Breaking! LinkedClass has one more argument in its constructor + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedClass.this"), ) val LinkerInterface = Seq( From bcce3f2dd415fd133db45d75073c08a490ee6b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 24 Jul 2022 15:05:27 +0200 Subject: [PATCH 38/75] Generate JSConstructorDef's in the compiler back-end. This commit complements the parent. We now generate `JSConstructorDef`s in the compiler back-end, and only enable the deserialization hack when reading IR version 1.8 and earlier. --- .../org/scalajs/nscplugin/GenJSCode.scala | 117 ++++++++++-------- .../scala/org/scalajs/ir/Serializers.scala | 2 +- .../scala/org/scalajs/ir/Transformers.scala | 3 + 3 files changed, 66 insertions(+), 56 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 78e6ac191b..9bca23caac 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -803,7 +803,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val privateFieldDefs = ListBuffer.empty[js.FieldDef] val classDefMembers = ListBuffer.empty[js.MemberDef] val instanceMembers = ListBuffer.empty[js.MemberDef] - var constructor: Option[js.JSMethodDef] = None + var constructor: Option[js.JSConstructorDef] = None origJsClass.memberDefs.foreach { case fdef: js.FieldDef => @@ -818,19 +818,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) classDefMembers += mdef case cdef: js.JSConstructorDef => - abort(s"we do not generate JSConstructorDef's yet, at ${cdef.pos}") + assert(constructor.isEmpty, "two ctors in class") + constructor = Some(cdef) case mdef: js.JSMethodDef => - mdef.name match { - case js.StringLiteral("constructor") => - assert(!mdef.flags.namespace.isStatic, "Exported static method") - assert(constructor.isEmpty, "two ctors in class") - constructor = Some(mdef) - - case _ => - assert(!mdef.flags.namespace.isStatic, "Exported static method") - instanceMembers += mdef - } + assert(!mdef.flags.namespace.isStatic, "Exported static method") + instanceMembers += mdef case property: js.JSPropertyDef => instanceMembers += property @@ -861,7 +854,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) throw new AssertionError( s"no class captures for anonymous JS class at $pos") } - val js.JSMethodDef(_, _, ctorParams, ctorRestParam, ctorBody) = constructor.getOrElse { + val js.JSConstructorDef(_, ctorParams, ctorRestParam, ctorBody) = constructor.getOrElse { throw new AssertionError("No ctor found") } assert(ctorParams.isEmpty && ctorRestParam.isEmpty, @@ -972,37 +965,44 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } // Transform the constructor body. - val inlinedCtorStats = new ir.Transformers.Transformer { - override def transform(tree: js.Tree, isStat: Boolean): js.Tree = tree match { - // The super constructor call. Transform this into a simple new call. - case js.JSSuperConstructorCall(args) => - implicit val pos = tree.pos + val inlinedCtorStats = { + val beforeSuper = ctorBody.beforeSuper + + val superCall = { + implicit val pos = ctorBody.superCall.pos + val js.JSSuperConstructorCall(args) = ctorBody.superCall + + val newTree = { + val ident = + origJsClass.superClass.getOrElse(abort("No superclass")) + if (args.isEmpty && ident.name == JSObjectClassName) + js.JSObjectConstr(Nil) + else + js.JSNew(jsSuperClassRef, args) + } - val newTree = { - val ident = - origJsClass.superClass.getOrElse(abort("No superclass")) - if (args.isEmpty && ident.name == JSObjectClassName) - js.JSObjectConstr(Nil) - else - js.JSNew(jsSuperClassRef, args) - } + val selfVarDef = js.VarDef(selfName, thisOriginalName, jstpe.AnyType, mutable = false, newTree) + selfVarDef :: memberDefinitions + } - js.Block( - js.VarDef(selfName, thisOriginalName, jstpe.AnyType, - mutable = false, newTree) :: - memberDefinitions)(NoPosition) + // 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 { + case js.This() => + selfRef(tree.pos) - 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) - // 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) + } + }.transformStats(ctorBody.afterSuper) - case tree => - super.transform(tree, isStat) - } - }.transform(ctorBody, isStat = true) + beforeSuper ::: superCall ::: afterSuper + } val closure = js.Closure(arrow = true, jsClassCaptures, Nil, None, js.Block(inlinedCtorStats, selfRef), jsSuperClassValue :: args) @@ -1431,7 +1431,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) // Constructor of a non-native JS class ------------------------------ def genJSClassCapturesAndConstructor(constructorTrees: List[DefDef])( - implicit pos: Position): (List[js.ParamDef], js.JSMethodDef) = { + implicit pos: Position): (List[js.ParamDef], js.JSConstructorDef) = { /* We need to merge all Scala constructors into a single one because * JavaScript only allows a single one. * @@ -1504,20 +1504,21 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) (exports.result(), jsClassCaptures.result()) } + // The name 'constructor' is used for error reporting here val (formalArgs, restParam, overloadDispatchBody) = genOverloadDispatch(JSName.Literal("constructor"), exports, jstpe.IntType) val overloadVar = js.VarDef(freshLocalIdent("overload"), NoOriginalName, jstpe.IntType, mutable = false, overloadDispatchBody) - val ctorStats = genJSClassCtorStats(overloadVar.ref, ctorTree) - - val constructorBody = js.Block( - paramVarDefs ::: List(overloadVar, ctorStats, js.Undefined())) + val constructorBody = wrapJSCtorBody( + paramVarDefs :+ overloadVar, + genJSClassCtorBody(overloadVar.ref, ctorTree), + js.Undefined() :: Nil + ) - val constructorDef = js.JSMethodDef( - js.MemberFlags.empty, - js.StringLiteral("constructor"), + val constructorDef = js.JSConstructorDef( + js.MemberFlags.empty.withNamespace(js.MemberNamespace.Constructor), formalArgs, restParam, constructorBody)(OptimizerHints.empty, None) (jsClassCaptures, constructorDef) @@ -1562,7 +1563,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) s"construtor at ${dd.pos}") new PrimaryJSCtor(sym, genParamsAndInfo(sym, vparamss), - jsSuperCall.get :: jsStats.result()) + js.JSConstructorBody(Nil, jsSuperCall.get, jsStats.result())(dd.pos)) } private def genSecondaryJSClassCtor(dd: DefDef): SplitSecondaryJSCtor = { @@ -1662,9 +1663,9 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) (jsExport, jsClassCaptures) } - /** generates a sequence of JS constructor statements based on a constructor tree. */ - private def genJSClassCtorStats(overloadVar: js.VarRef, - ctorTree: ConstructorTree[PrimaryJSCtor])(implicit pos: Position): js.Tree = { + /** Generates a JS constructor body based on a constructor tree. */ + private def genJSClassCtorBody(overloadVar: js.VarRef, + ctorTree: ConstructorTree[PrimaryJSCtor])(implicit pos: Position): js.JSConstructorBody = { /* generates a statement that conditionally executes body iff the chosen * overload is any of the descendants of `tree` (including itself). @@ -1761,13 +1762,19 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val primaryCtor = ctorTree.ctor val secondaryCtorTrees = ctorTree.subCtors - js.Block( - secondaryCtorTrees.map(preStats(_, primaryCtor.paramsAndInfo)) ++ - primaryCtor.body ++ + wrapJSCtorBody( + secondaryCtorTrees.map(preStats(_, primaryCtor.paramsAndInfo)), + primaryCtor.body, secondaryCtorTrees.map(postStats(_)) ) } + private def wrapJSCtorBody(before: List[js.Tree], body: js.JSConstructorBody, + after: List[js.Tree]): js.JSConstructorBody = { + js.JSConstructorBody(before ::: body.beforeSuper, body.superCall, + body.afterSuper ::: after)(body.pos) + } + private sealed trait JSCtor { val sym: Symbol val paramsAndInfo: List[(js.VarRef, JSParamInfo)] @@ -1775,7 +1782,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private class PrimaryJSCtor(val sym: Symbol, val paramsAndInfo: List[(js.VarRef, JSParamInfo)], - val body: List[js.Tree]) extends JSCtor + val body: js.JSConstructorBody) extends JSCtor private class SplitSecondaryJSCtor(val sym: Symbol, val paramsAndInfo: List[(js.VarRef, JSParamInfo)], 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 93233992bb..219bacb632 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -1257,7 +1257,7 @@ object Serializers { val optimizerHints = OptimizerHints.fromBits(readInt()) val memberDefs = - if (/*hacks.use8 &&*/ kind.isJSClass) memberDefs0.map(jsConstructorDefHack(_)) // scalastyle:ignore + if (hacks.use8 && kind.isJSClass) memberDefs0.map(jsConstructorDefHack(_)) else memberDefs0 ClassDef(name, originalName, kind, jsClassCaptures, superClass, parents, 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 69b58dbe22..f529594c97 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -18,6 +18,9 @@ import Types._ 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) From 98384257ffdfddccadfa5428dc2468927c376203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 4 Aug 2022 17:27:28 +0200 Subject: [PATCH 39/75] Fix #4705: Avoid emitting identifiers called 'await'. 'await' is conditionally reserved in ECMAScript, namely when it is within an ES Module. --- .../scalajs/linker/backend/emitter/NameGen.scala | 16 +++++++++------- .../frontend/optimizer/OptimizerCore.scala | 10 +++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala index c2e9393b79..3f21ea8adb 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala @@ -355,6 +355,8 @@ private object NameGen { * - All ECMAScript 2015 keywords; * - Identifier names that are treated as keywords in ECMAScript 2015 * Strict Mode; + * - Identifier names that are treated as keywords in some contexts, such as + * ES modules; * - The identifiers `arguments` and `eval`, because they cannot be used for * local variable names in ECMAScript 2015 Strict Mode; * - The identifier `undefined`, because that's way too confusing if it does @@ -362,13 +364,13 @@ private object NameGen { * cliffs we can trigger with that. */ private final val ReservedJSIdentifierNames: Set[String] = Set( - "arguments", "break", "case", "catch", "class", "const", "continue", - "debugger", "default", "delete", "do", "else", "enum", "eval", "export", - "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", "undefined", "var", - "void", "while", "with", "yield" + "arguments", "await", "break", "case", "catch", "class", "const", + "continue", "debugger", "default", "delete", "do", "else", "enum", + "eval", "export", "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", "undefined", + "var", "void", "while", "with", "yield" ) private val compressedPrefixes: List[(UTF8String, String)] = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index fcad53fdbe..b04210760b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -5954,11 +5954,11 @@ private[optimizer] object OptimizerCore { * (with ASCII characters only). */ private val EmitterReservedJSIdentifiers = List( - "arguments", "break", "case", "catch", "class", "const", "continue", - "debugger", "default", "delete", "do", "else", "enum", "eval", - "export", "extends", "false", "finally", "for", "function", "if", - "implements", "import", "in", "instanceof", "interface", "let", "new", - "null", "package", "private", "protected", "public", "return", + "arguments", "await", "break", "case", "catch", "class", "const", + "continue", "debugger", "default", "delete", "do", "else", "enum", + "eval", "export", "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", "undefined", "var", "void", "while", "with", "yield" ) From 0265cd6a6280b88a0d19b0382af80d8869827821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 4 Aug 2022 17:53:51 +0200 Subject: [PATCH 40/75] Warn when using 'await' as a global ref. It is a reserved word within an ES module. We cannot make it invalid for backward compatibility reasons, so we deprecate it instead. This complements the parent commit, which avoided to emit 'await' for generated code. --- .../scalajs/nscplugin/CompatComponent.scala | 1 + .../org/scalajs/nscplugin/GenJSCode.scala | 86 ++++++-------- .../scalajs/nscplugin/test/JSExportTest.scala | 6 - .../nscplugin/test/JSGlobalScopeTest.scala | 107 ++++++++++++++++++ .../nscplugin/test/util/TestHelpers.scala | 6 + .../src/main/scala/org/scalajs/ir/Trees.scala | 5 + 6 files changed, 156 insertions(+), 55 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala b/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala index f208b05c3d..dad3818c3c 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/CompatComponent.scala @@ -127,6 +127,7 @@ trait CompatComponent { object WarningCategoryCompat { object Reporting { object WarningCategory { + val Deprecation: Any = null val Other: Any = null } } diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 3ad1ccabec..67ec33459e 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -6639,31 +6639,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.JSSelect(qualTree, item) case MaybeGlobalScope.GlobalScope(_) => - item match { - case js.StringLiteral(value) => - if (js.JSGlobalRef.isValidJSGlobalRefName(value)) { - js.JSGlobalRef(value) - } else if (js.JSGlobalRef.ReservedJSIdentifierNames.contains(value)) { - reporter.error(pos, - "Invalid selection in the global scope of the reserved " + - s"identifier name `$value`." + - GenericGlobalObjectInformationMsg) - js.JSGlobalRef("erroneous") - } else { - reporter.error(pos, - "Selecting a field of the global scope whose name is " + - "not a valid JavaScript identifier is not allowed." + - GenericGlobalObjectInformationMsg) - js.JSGlobalRef("erroneous") - } - - case _ => - reporter.error(pos, - "Selecting a field of the global scope with a dynamic " + - "name is not allowed." + - GenericGlobalObjectInformationMsg) - js.JSGlobalRef("erroneous") - } + genJSGlobalRef(item, "Selecting a field", "selection") } } @@ -6686,31 +6662,43 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.JSMethodApply(receiverTree, method, args) case MaybeGlobalScope.GlobalScope(_) => - method match { - case js.StringLiteral(value) => - if (js.JSGlobalRef.isValidJSGlobalRefName(value)) { - js.JSFunctionApply(js.JSGlobalRef(value), args) - } else if (js.JSGlobalRef.ReservedJSIdentifierNames.contains(value)) { - reporter.error(pos, - "Invalid call in the global scope of the reserved " + - s"identifier name `$value`." + - GenericGlobalObjectInformationMsg) - js.Undefined() - } else { - reporter.error(pos, - "Calling a method of the global scope whose name is not " + - "a valid JavaScript identifier is not allowed." + - GenericGlobalObjectInformationMsg) - js.Undefined() - } - - case _ => - reporter.error(pos, - "Calling a method of the global scope with a dynamic " + - "name is not allowed." + - GenericGlobalObjectInformationMsg) - js.Undefined() + val globalRef = genJSGlobalRef(method, "Calling a method", "call") + js.JSFunctionApply(globalRef, args) + } + } + + private def genJSGlobalRef(propName: js.Tree, + actionFull: String, actionSimpleNoun: String)( + implicit pos: Position): js.JSGlobalRef = { + propName match { + case js.StringLiteral(value) => + if (js.JSGlobalRef.isValidJSGlobalRefName(value)) { + if (value == "await") { + global.runReporting.warning(pos, + s"$actionFull of the global scope with the name '$value' is deprecated.\n" + + " It may produce invalid JavaScript code causing a SyntaxError in some environments." + + GenericGlobalObjectInformationMsg, + WarningCategory.Deprecation, + currentMethodSym.get) + } + js.JSGlobalRef(value) + } else if (js.JSGlobalRef.ReservedJSIdentifierNames.contains(value)) { + reporter.error(pos, + s"Invalid $actionSimpleNoun in the global scope of the reserved identifier name `$value`." + + GenericGlobalObjectInformationMsg) + js.JSGlobalRef("erroneous") + } else { + reporter.error(pos, + s"$actionFull of the global scope whose name is not a valid JavaScript identifier is not allowed." + + GenericGlobalObjectInformationMsg) + js.JSGlobalRef("erroneous") } + + case _ => + reporter.error(pos, + s"$actionFull of the global scope with a dynamic name is not allowed." + + GenericGlobalObjectInformationMsg) + js.JSGlobalRef("erroneous") } } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSExportTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSExportTest.scala index b61acd71ed..790edb15b6 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSExportTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSExportTest.scala @@ -1033,12 +1033,6 @@ class JSExportTest extends DirectTest with TestHelpers { } - private def since(v: String): String = { - val version = scala.util.Properties.versionNumberString - if (version.startsWith("2.11.")) "" - else s" (since $v)" - } - @Test def noExportTopLevelTrait(): Unit = { """ 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 5041885be3..eb48002a23 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/JSGlobalScopeTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/JSGlobalScopeTest.scala @@ -13,14 +13,19 @@ package org.scalajs.nscplugin.test import org.scalajs.nscplugin.test.util._ +import org.scalajs.nscplugin.test.util.VersionDependentUtils.scalaSupportsNoWarn import org.junit.Test import org.junit.Ignore +import org.junit.Assume._ // scalastyle:off line.size.limit class JSGlobalScopeTest extends DirectTest with TestHelpers { + override def extraArgs: List[String] = + super.extraArgs :+ "-deprecation" + override def preamble: String = { """ import scala.scalajs.js @@ -244,6 +249,9 @@ class JSGlobalScopeTest extends DirectTest with TestHelpers { } """ hasErrors s""" + |newSource1.scala:41: warning: method apply in object global is deprecated${since("forever")}: The global scope cannot be called as function. + | val a = js.Dynamic.global(3) + | ^ |newSource1.scala:41: error: Loading the global scope as a value (anywhere but as the left-hand-side of a `.`-selection) is not allowed. | See https://www.scala-js.org/doc/interoperability/global-scope.html for further information. | val a = js.Dynamic.global(3) @@ -390,4 +398,103 @@ class JSGlobalScopeTest extends DirectTest with TestHelpers { } } + @Test + def warnAboutAwaitReservedWord_Issue4705(): Unit = { + val reservedIdentifiers = List("await") + + for (reservedIdentifier <- reservedIdentifiers) { + val spaces = " " * reservedIdentifier.length() + + s""" + @js.native + @JSGlobalScope + object CustomGlobalScope extends js.Any { + var `$reservedIdentifier`: Int = js.native + @JSName("$reservedIdentifier") + def `${reservedIdentifier}2`(x: Int): Int = js.native + } + + object Main { + def main(): Unit = { + val a = js.Dynamic.global.`$reservedIdentifier` + js.Dynamic.global.`$reservedIdentifier` = 5 + val b = js.Dynamic.global.`$reservedIdentifier`(5) + + val c = CustomGlobalScope.`$reservedIdentifier` + CustomGlobalScope.`$reservedIdentifier` = 5 + val d = CustomGlobalScope.`${reservedIdentifier}2`(5) + } + } + """ hasWarns + s""" + |newSource1.scala:49: warning: Selecting a field of the global scope with the name '$reservedIdentifier' is deprecated. + | It may produce invalid JavaScript code causing a SyntaxError in some environments. + | See https://www.scala-js.org/doc/interoperability/global-scope.html for further information. + | val a = js.Dynamic.global.`$reservedIdentifier` + | ^ + |newSource1.scala:50: warning: Selecting a field of the global scope with the name '$reservedIdentifier' is deprecated. + | It may produce invalid JavaScript code causing a SyntaxError in some environments. + | See https://www.scala-js.org/doc/interoperability/global-scope.html for further information. + | js.Dynamic.global.`$reservedIdentifier` = 5 + | ^ + |newSource1.scala:51: warning: Calling a method of the global scope with the name '$reservedIdentifier' is deprecated. + | It may produce invalid JavaScript code causing a SyntaxError in some environments. + | See https://www.scala-js.org/doc/interoperability/global-scope.html for further information. + | val b = js.Dynamic.global.`$reservedIdentifier`(5) + | $spaces^ + |newSource1.scala:53: warning: Selecting a field of the global scope with the name '$reservedIdentifier' is deprecated. + | It may produce invalid JavaScript code causing a SyntaxError in some environments. + | See https://www.scala-js.org/doc/interoperability/global-scope.html for further information. + | val c = CustomGlobalScope.`$reservedIdentifier` + | ^ + |newSource1.scala:54: warning: Selecting a field of the global scope with the name '$reservedIdentifier' is deprecated. + | It may produce invalid JavaScript code causing a SyntaxError in some environments. + | See https://www.scala-js.org/doc/interoperability/global-scope.html for further information. + | CustomGlobalScope.`$reservedIdentifier` = 5 + | $spaces^ + |newSource1.scala:55: warning: Calling a method of the global scope with the name '$reservedIdentifier' is deprecated. + | It may produce invalid JavaScript code causing a SyntaxError in some environments. + | See https://www.scala-js.org/doc/interoperability/global-scope.html for further information. + | val d = CustomGlobalScope.`${reservedIdentifier}2`(5) + | $spaces^ + """ + } + } + + @Test + def noWarnAboutAwaitReservedWordIfSelectivelyDisabled(): Unit = { + assumeTrue(scalaSupportsNoWarn) + + val reservedIdentifiers = List("await") + + for (reservedIdentifier <- reservedIdentifiers) { + val spaces = " " * reservedIdentifier.length() + + s""" + import scala.annotation.nowarn + + @js.native + @JSGlobalScope + object CustomGlobalScope extends js.Any { + var `$reservedIdentifier`: Int = js.native + @JSName("$reservedIdentifier") + def `${reservedIdentifier}2`(x: Int): Int = js.native + } + + object Main { + @nowarn("cat=deprecation") + def main(): Unit = { + val a = js.Dynamic.global.`$reservedIdentifier` + js.Dynamic.global.`$reservedIdentifier` = 5 + val b = js.Dynamic.global.`$reservedIdentifier`(5) + + val c = CustomGlobalScope.`$reservedIdentifier` + CustomGlobalScope.`$reservedIdentifier` = 5 + val d = CustomGlobalScope.`${reservedIdentifier}2`(5) + } + } + """.hasNoWarns() + } + } + } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/util/TestHelpers.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/util/TestHelpers.scala index 112b9aad99..8336f2e472 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/util/TestHelpers.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/util/TestHelpers.scala @@ -32,6 +32,12 @@ trait TestHelpers extends DirectTest { /** will be prefixed to every code that is compiled. use for imports */ def preamble: String = "" + protected def since(v: String): String = { + val version = scala.util.Properties.versionNumberString + if (version.startsWith("2.11.")) "" + else s" (since $v)" + } + /** pimps a string to compile it and apply the specified test */ implicit class CompileTests(val code: String) { private lazy val (success, output) = { 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 a8b457be3d..11e4a67131 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -863,6 +863,11 @@ object Trees { * - The identifier `arguments`, because any attempt to refer to it always * refers to the magical `arguments` pseudo-array from the enclosing * function, rather than a global variable. + * + * This set does *not* contain `await`, although it is a reserved word + * within ES modules. It used to be allowed before 1.11.0, and even + * browsers do not seem to reject it. For compatibility reasons, we only + * warn about it at compile time, but the IR allows it. */ final val ReservedJSIdentifierNames: Set[String] = Set( "arguments", "break", "case", "catch", "class", "const", "continue", From 1770544f09e9830dd51b6d1b0334c5e95d93bd03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 24 Jul 2022 16:27:27 +0200 Subject: [PATCH 41/75] Actually handle regular methods named 'constructor' in the emitter. --- .../linker/backend/emitter/ClassEmitter.scala | 10 ++++++- .../jsinterop/NonNativeJSTypeTest.scala | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala index 8e42ee7365..40cb4f2460 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala @@ -696,8 +696,16 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genMemberNameTree(name: Tree)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge): WithGlobals[js.PropertyName] = { + /* When it's a string literal but not "constructor", we can directly use a + * string literal in the ES class definition. Otherwise, we must wrap the + * expression in a `js.ComputedName`, which is `[tree]` in ES syntax. + * We must exclude "constructor" because that would represent the actual + * ES class constructor (which is taken care of by a JSConstructorDef), + * whereas `["constructor"]` represents a non-constructor method called + * "constructor". + */ name match { - case StringLiteral(value) => + case StringLiteral(value) if value != "constructor" => WithGlobals(js.StringLiteral(value)(name.pos)) case _ => diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala index a4c66097e4..94cdf52847 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala @@ -201,6 +201,26 @@ class NonNativeJSTypeTest { assertEquals(List(1, 2, 3, 4, 5, 6), obj.allArgs) } + @Test def methodNamedConstructor(): Unit = { + val obj1 = new MethodNamedConstructor(5) + assertEquals(5, obj1.x) + assertEquals(7, obj1.constructor(2)) + assertNotSame(js.constructorOf[MethodNamedConstructor], obj1.asInstanceOf[js.Dynamic].constructor) + + val obj2 = new SubclassOfMethodNamedConstructor(11, 15) + assertEquals(11, obj2.x) + assertEquals(15, obj2.z) + assertEquals(13, obj2.constructor(2)) + assertEquals("foo 15", obj2.constructor("foo ")) + + // Undesirable behavior, but the same as what would happen if we did it in JavaScript + val obj3 = new SubclassOfMethodNamedConstructorNoRedefine(42) + assertSame(js.constructorOf[SubclassOfMethodNamedConstructorNoRedefine], + obj3.asInstanceOf[js.Dynamic].constructor) + if (Platform.useECMAScript2015Semantics) + assertThrows(classOf[Exception], obj3.constructor(1)) + } + @Test def defaultValuesForFields(): Unit = { val obj = new DefaultFieldValues assertEquals(0, obj.int) @@ -2088,6 +2108,16 @@ object NonNativeJSTypeTest { val allArgs = List(arg, arg$1, arg$2, prep, prep$1, prep$2) } + class MethodNamedConstructor(val x: Int) extends js.Object { + def constructor(y: Int): Int = x + y + } + + class SubclassOfMethodNamedConstructor(x: Int, val z: Int) extends MethodNamedConstructor(x) { + def constructor(y: String): String = y + z + } + + class SubclassOfMethodNamedConstructorNoRedefine(x: Int) extends MethodNamedConstructor(x) + class DefaultFieldValues extends js.Object { var int: Int = _ var bool: Boolean = _ From 874d9b95bf5fcf3accf6bd52e5e5332da2431775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 5 Aug 2022 14:48:32 +0200 Subject: [PATCH 42/75] In methods of hijacked classes, `this` can be typed as the primitive. Within an instance method, we know that `this` is non-null. For hijacked classes, that means we can type it as the corresponding primitive type. This improves the generated code for hijacked class methods, as they do not need to unbox their `$thiz` parameter anymore. This has a particular importance for `j.l.Character` methods, as they now take a primitive number instead of a boxed instance. This change of "private ABI" causes further changes in the codegen of dispatchers and of direct calls to those methods. This is a backward binary compatible change because the IR checker only requires that the type of `This()` nodes be a supertype of the expected one. --- .../org/scalajs/nscplugin/GenJSCode.scala | 19 +++++--- .../src/main/scala/org/scalajs/ir/Types.scala | 41 +++++++++-------- .../linker/backend/emitter/CoreJSLib.scala | 8 +++- .../backend/emitter/FunctionEmitter.scala | 46 +++++++++++++++++-- .../scalajs/linker/checker/IRChecker.scala | 10 ++-- .../frontend/optimizer/IncOptimizer.scala | 1 + .../frontend/optimizer/OptimizerCore.scala | 34 +++++++++----- 7 files changed, 112 insertions(+), 47 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 893778cea2..8f08fb4a12 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -151,7 +151,14 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private val fieldsMutatedInCurrentClass = new ScopedVar[mutable.Set[Name]] private val generatedSAMWrapperCount = new ScopedVar[VarBox[Int]] - private def currentClassType = encodeClassType(currentClassSym) + private def currentThisType: jstpe.Type = { + encodeClassType(currentClassSym) match { + case tpe @ jstpe.ClassType(cls) => + jstpe.BoxedClassToPrimType.getOrElse(cls, tpe) + case tpe => + tpe + } + } // Per method body private val currentMethodSym = new ScopedVar[Symbol] @@ -1927,7 +1934,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } Some(genApplyStatic(implMethodSym, - js.This()(currentClassType) :: jsParams.map(_.ref))) + js.This()(currentThisType) :: jsParams.map(_.ref))) } else { None } @@ -2179,7 +2186,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) mutableLocalVars += thisSym val thisLocalIdent = encodeLocalSym(thisSym) - val thisLocalType = currentClassType + val thisLocalType = currentThisType val genRhs = { /* #3267 In default methods, scalac will type its _$this @@ -2660,14 +2667,14 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) throw new CancelGenMethodAsJSFunction( "Trying to generate `this` inside the body") } - js.This()(currentClassType) + js.This()(currentThisType) } { thisLocalIdent => // .copy() to get the correct position val tpe = { if (isImplClass(currentClassSym)) encodeClassType(traitOfImplClass(currentClassSym)) else - currentClassType + currentThisType } js.VarRef(thisLocalIdent.copy())(tpe) } @@ -3337,7 +3344,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val isTailJumpThisLocalVar = formalArgSym.name == nme.THIS val tpe = - if (isTailJumpThisLocalVar) currentClassType + if (isTailJumpThisLocalVar) currentThisType else toIRType(formalArgSym.tpe) val fixedActualArg = 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 d360559d0f..a82570a840 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Types.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Types.scala @@ -297,6 +297,22 @@ object Types { 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. @@ -316,26 +332,11 @@ object Types { case (NullType, ClassType(_)) => true case (NullType, ArrayType(_)) => true - case (UndefType, ClassType(className)) => - isSubclass(BoxedUnitClass, className) - case (BooleanType, ClassType(className)) => - isSubclass(BoxedBooleanClass, className) - case (CharType, ClassType(className)) => - isSubclass(BoxedCharacterClass, className) - case (ByteType, ClassType(className)) => - isSubclass(BoxedByteClass, className) - case (ShortType, ClassType(className)) => - isSubclass(BoxedShortClass, className) - case (IntType, ClassType(className)) => - isSubclass(BoxedIntegerClass, className) - case (LongType, ClassType(className)) => - isSubclass(BoxedLongClass, className) - case (FloatType, ClassType(className)) => - isSubclass(BoxedFloatClass, className) - case (DoubleType, ClassType(className)) => - isSubclass(BoxedDoubleClass, className) - case (StringType, ClassType(className)) => - isSubclass(BoxedStringClass, className) + case (primType: PrimType, ClassType(rhsClass)) => + val lhsClass = PrimTypeToBoxedClass.getOrElse(primType, { + throw new AssertionError(s"unreachable case for isSubtype($lhs, $rhs)") + }) + isSubclass(lhsClass, rhsClass) case (ArrayType(ArrayTypeRef(lhsBase, lhsDims)), ArrayType(ArrayTypeRef(rhsBase, rhsDims))) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index f588ec11f3..bfebd9324f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -794,8 +794,12 @@ private[emitter] object CoreJSLib { case _ => None } - def genHijackedMethodApply(className: ClassName): Tree = - Apply(globalVar("f", (className, methodName)), instance :: args) + def genHijackedMethodApply(className: ClassName): Tree = { + val instanceAsPrimitive = + if (className == BoxedCharacterClass) genCallHelper("uC", instance) + else instance + Apply(globalVar("f", (className, methodName)), instanceAsPrimitive :: args) + } def genBodyNoSwitch(hijackedClasses: List[ClassName]): Tree = { val normalCall = Return(Apply(instance DOT genName(methodName), args)) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index 465534634d..c82ef8e9a5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -2217,17 +2217,38 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Apply(_, receiver, method, args) => val methodName = method.name - val newReceiver = transformExprNoChar(receiver) + + def newReceiver(asChar: Boolean): js.Tree = { + if (asChar) { + /* When statically calling a (hijacked) method of j.l.Character, + * the receiver must be passed as a primitive CharType. If it is + * not already a CharType, we must introduce a cast to unbox the + * value. + */ + if (realTreeType(receiver) == CharType) + transformExpr(receiver, preserveChar = true) + else + transformExpr(AsInstanceOf(receiver, CharType), preserveChar = true) + } else { + /* For other primitive types, unboxes/casts are not necessary, + * because they would only convert `null` to the zero value of + * the type. However, calling a method on `null` is UB, so we + * need not do anything about it. + */ + transformExprNoChar(receiver) + } + } + val newArgs = transformTypedArgs(method.name, args) def genNormalApply(): js.Tree = - js.Apply(newReceiver DOT transformMethodIdent(method), newArgs) + js.Apply(newReceiver(false) DOT transformMethodIdent(method), newArgs) def genDispatchApply(): js.Tree = - genCallHelper("dp_" + genName(methodName), newReceiver :: newArgs: _*) + genCallHelper("dp_" + genName(methodName), newReceiver(false) :: newArgs: _*) def genHijackedMethodApply(className: ClassName): js.Tree = - genApplyStaticLike("f", className, method, newReceiver :: newArgs) + genApplyStaticLike("f", className, method, newReceiver(className == BoxedCharacterClass) :: newArgs) if (isMaybeHijackedClass(receiver.tpe) && !methodName.isReflectiveProxy) { @@ -2944,12 +2965,27 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { tree.getClass) } - if (preserveChar || tree.tpe != CharType) + if (preserveChar || realTreeType(tree) != CharType) baseResult else genCallHelper("bC", baseResult) } + private def realTreeType(tree: Tree)(implicit env: Env): Type = { + val tpe = tree.tpe + tree match { + case This() if tpe.isInstanceOf[ClassType] => + env.enclosingClassName match { + case Some(enclosingClassName) => + BoxedClassToPrimType.getOrElse(enclosingClassName, ClassType(enclosingClassName)) + case None => + tpe + } + case _ => + tpe + } + } + private def transformApplyDynamicImport(tree: ApplyDynamicImport)( implicit env: Env): js.Tree = { implicit val pos = tree.pos diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 0d9cdbbac1..096ce7e83f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -128,7 +128,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { } { body => val thisType = if (static) NoType - else ClassType(classDef.name.name) + else thisTypeForScalaClass(classDef) val inConstructorOf = if (flags.namespace.isConstructor) Some(classDef.name.name) @@ -171,7 +171,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { val thisType = { if (static) NoType else if (clazz.kind.isJSClass) AnyType - else ClassType(clazz.name.name) + else thisTypeForScalaClass(clazz) } val bodyEnv = Env.fromSignature(thisType) @@ -188,7 +188,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { val thisType = if (flags.namespace.isStatic) NoType else if (clazz.kind.isJSClass) AnyType - else ClassType(clazz.name.name) + else thisTypeForScalaClass(clazz) val env = Env.fromSignature(thisType) @@ -199,6 +199,10 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { } } + private def thisTypeForScalaClass(clazz: LinkedClass): Type = + if (clazz.kind == ClassKind.HijackedClass) BoxedClassToPrimType(clazz.name.name) + else ClassType(clazz.name.name) + private def typecheckExpect(tree: Tree, env: Env, expectedType: Type)( implicit ctx: ErrorContext): Unit = { typecheck(tree, env) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index f237114c67..16de081a94 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -240,6 +240,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: def thisType: Type = if (namespace.isStatic) NoType + else if (linkedClass.kind == ClassKind.HijackedClass) BoxedClassToPrimType(className) else ClassType(className) val methods = mutable.Map.empty[MethodName, MethodImpl] diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index b04210760b..6a4d218dd6 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -562,7 +562,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case AsInstanceOf(arg, tpe) => trampoline { pretransformExpr(arg) { targ => - foldAsInstanceOf(targ, tpe)(finishTransform(isStat)) + finishTransform(isStat)(foldAsInstanceOf(targ, tpe)) } } @@ -1037,7 +1037,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case AsInstanceOf(expr, tpe) => pretransformExpr(expr) { texpr => - foldAsInstanceOf(texpr, tpe)(cont) + cont(foldAsInstanceOf(texpr, tpe)) } case Closure(arrow, captureParams, params, restParam, body, captureValues) => @@ -1671,7 +1671,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { /* When inlining a single method, the declared type of the `this` * value is its enclosing class. */ - val receiverType = ClassType(target.enclosingClassName) + val receiverType = receiverTypeFor(target) inline(allocationSites, Some((receiverType, treceiver)), targs, target, isStat, usePreTransform)(cont) } else { @@ -1795,7 +1795,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { scope.implsBeingInlined((allocationSites, target)) if (shouldInline && !beingInlined) { - val receiverType = ClassType(target.enclosingClassName) + val receiverType = receiverTypeFor(target) inline(allocationSites, Some((receiverType, treceiver)), targs, target, isStat, usePreTransform)(cont) } else { @@ -1807,6 +1807,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } } + private def receiverTypeFor(target: MethodID): Type = + BoxedClassToPrimType.getOrElse(target.enclosingClassName, ClassType(target.enclosingClassName)) + private def pretransformApplyStatic(tree: ApplyStatic, isStat: Boolean, usePreTransform: Boolean)( cont: PreTransCont)( @@ -2237,7 +2240,17 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { implicit scope: Scope, pos: Position): TailRec[Tree] = tailcall { val optReceiverBinding = optReceiver map { receiver => - Binding(Binding.This, receiver._1, false, receiver._2) + /* If the declaredType is CharType, we must introduce a cast, because we + * must communicate to the emitter that it has to unbox the value. + * For other primitive types, unboxes/casts are not necessary, because + * they would only convert `null` to the zero value of the type. However, + * calling a method on `null` is UB, so we need not do anything about it. + */ + val (declaredType, value0) = receiver + val value = + if (declaredType == CharType) foldAsInstanceOf(value0, declaredType) + else value0 + Binding(Binding.This, declaredType, false, value) } assert(formals.size == args.size, @@ -2316,9 +2329,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { val index = finishTransformExpr(tindex) val elemType = cursoryArrayElemType(arrayTpe) val select = ArraySelect(array, index)(elemType) - foldAsInstanceOf(tvalue, elemType) { tunboxedValue => - contTree(Assign(select, finishTransformExpr(tunboxedValue))) - } + val tunboxedValue = foldAsInstanceOf(tvalue, elemType) + contTree(Assign(select, finishTransformExpr(tunboxedValue))) case _ => defaultApply(AnyType) } @@ -4202,11 +4214,11 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } private def foldAsInstanceOf(arg: PreTransform, tpe: Type)( - cont: PreTransCont): TailRec[Tree] = { + implicit pos: Position): PreTransform = { if (isSubtype(arg.tpe.base, tpe)) - cont(arg) + arg else - cont(AsInstanceOf(finishTransformExpr(arg), tpe)(arg.pos).toPreTransform) + AsInstanceOf(finishTransformExpr(arg), tpe).toPreTransform } private def foldJSSelect(qualifier: Tree, item: Tree)( From 71e473fd415ac63f992915fc53ce930fe800567b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 9 Aug 2022 10:18:28 +0200 Subject: [PATCH 43/75] Fix the generation of private "symbols" in the ES 5.1 polyfills. They were generating fractional values (in `PrivateFieldsSymbolHolder`) and negative values (in the `CoreJSLib` polyfill for `PrivateSymbolBuiltin`), which, when converted to strings, would not follow the expected scheme of 1 to 8 hexadecimal digits. This was not fundamentally broken, as they remained valid property names, but was inelegant. We fix both by using a `>>> 0` conversion to an unsigned 32-bit integer. Moreover, the variant in `PrivateFieldsSymbolHolder` would actually cause issues with checked `StringIndexOutOfBoundsException`s, since it used `String.substring` with a bigger-than-expected index. We take the opportunity to switch it to `jsSubstring` as well, as we want this polyfill to be as primitive as possible. --- .../scala/scalajs/runtime/PrivateFieldsSymbolHolder.scala | 5 +++-- .../org/scalajs/linker/backend/emitter/CoreJSLib.scala | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/library/src/main/scala/scala/scalajs/runtime/PrivateFieldsSymbolHolder.scala b/library/src/main/scala/scala/scalajs/runtime/PrivateFieldsSymbolHolder.scala index 8e78f9e01f..a880135f4d 100644 --- a/library/src/main/scala/scala/scalajs/runtime/PrivateFieldsSymbolHolder.scala +++ b/library/src/main/scala/scala/scalajs/runtime/PrivateFieldsSymbolHolder.scala @@ -13,6 +13,7 @@ package scala.scalajs.runtime import scala.scalajs.js +import scala.scalajs.js.JSStringOps._ import scala.scalajs.LinkingInfo.ESVersion private[runtime] object PrivateFieldsSymbolHolder { @@ -23,9 +24,9 @@ private[runtime] object PrivateFieldsSymbolHolder { js.Symbol("privateFields") } else { def rand32(): String = { - val s = (js.Math.random() * 4294967296.0).asInstanceOf[js.Dynamic] + val s = ((js.Math.random() * 4294967296.0).asInstanceOf[js.Dynamic] >>> 0.asInstanceOf[js.Dynamic]) .applyDynamic("toString")(16).asInstanceOf[String] - "00000000".substring(s.length) + s + "00000000".jsSubstring(s.length) + s } rand32() + rand32() + rand32() + rand32() } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index bfebd9324f..725e158d04 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -365,7 +365,7 @@ private[emitter] object CoreJSLib { case PrivateSymbolBuiltin => /* function privateJSFieldSymbol(description) { * function rand32() { - * const s = ((Math.random() * 4294967296.0) | 0).toString(16); + * const s = ((Math.random() * 4294967296.0) >>> 0).toString(16); * return "00000000".substring(s.length) + s; * } * return description + rand32() + rand32() + rand32() + rand32(); @@ -386,9 +386,9 @@ private[emitter] object CoreJSLib { genLet(s.ident, mutable = false, { val randomDouble = Apply(genIdentBracketSelect(MathRef, "random"), Nil) - val randomInt = - (randomDouble * double(4294967296.0)) | 0 - Apply(genIdentBracketSelect(randomInt, "toString"), 16 :: Nil) + val randomUint = + (randomDouble * double(4294967296.0)) >>> 0 + Apply(genIdentBracketSelect(randomUint, "toString"), 16 :: Nil) }), { val padding = Apply( From 8e6e6db986497565418dbc2e0070ddb250184272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 8 Aug 2022 15:23:19 +0200 Subject: [PATCH 44/75] Do not rely on undefined behavior of `substring` in the javalib. Knowing how `String.substring` was implemented, we have been abusing it within the javalib with case where the `end` index was out of bounds. As we are going to make it optionally compliant, we cannot rely on that anymore. Instead, we directly use `jsSubstring`, or explicitly check the bounds ahead of time. --- .../src/main/scala/java/lang/StackTrace.scala | 6 ++-- .../src/main/scala/java/util/Formatter.scala | 2 +- .../java/util/regex/IndicesBuilder.scala | 17 +++++----- .../java/util/regex/PatternCompiler.scala | 31 ++++++++++--------- .../org/scalajs/linker/LibrarySizeTest.scala | 6 ++-- .../javalib/util/FormatterTest.scala | 7 +++++ 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/javalib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala index 465dd2e29a..0316c5c679 100644 --- a/javalib/src/main/scala/java/lang/StackTrace.scala +++ b/javalib/src/main/scala/java/lang/StackTrace.scala @@ -228,12 +228,12 @@ private[lang] object StackTrace { if (i < compressedPrefixes.length) { val prefix = compressedPrefixes(i) if (encodedName.startsWith(prefix)) - dictRawApply(decompressedPrefixes, prefix) + encodedName.substring(prefix.length) + dictRawApply(decompressedPrefixes, prefix) + encodedName.jsSubstring(prefix.length) else loop(i+1) } else { // no prefix matches - if (encodedName.startsWith("L")) encodedName.substring(1) + if (encodedName.startsWith("L")) encodedName.jsSubstring(1) else encodedName // just in case } } @@ -284,7 +284,7 @@ private[lang] object StackTrace { } else { val methodNameLen = encodedName.indexOf("__") if (methodNameLen < 0) encodedName - else encodedName.substring(0, methodNameLen) + else encodedName.jsSubstring(0, methodNameLen) } } diff --git a/javalib/src/main/scala/java/util/Formatter.scala b/javalib/src/main/scala/java/util/Formatter.scala index 7625e4fa44..b70237c115 100644 --- a/javalib/src/main/scala/java/util/Formatter.scala +++ b/javalib/src/main/scala/java/util/Formatter.scala @@ -751,7 +751,7 @@ final class Formatter private (private[this] var dest: Appendable, width: Int, precision: Int, str: String): Unit = { val truncatedStr = - if (precision < 0) str + if (precision < 0 || precision >= str.length()) str else str.substring(0, precision) padAndSendToDestNoZeroPad(flags, width, applyUpperCase(localeInfo, flags, truncatedStr)) diff --git a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala index e493d51fc2..85216af945 100644 --- a/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala +++ b/javalib/src/main/scala/java/util/regex/IndicesBuilder.scala @@ -17,6 +17,7 @@ import scala.annotation.{tailrec, switch} import java.lang.Utils._ import scala.scalajs.js +import scala.scalajs.js.JSStringOps._ import Pattern.IndicesArray @@ -419,7 +420,7 @@ private[regex] object IndicesBuilder { } case '(' => - val indicator = pattern.substring(pIndex + 1, pIndex + 3) + val indicator = pattern.jsSubstring(pIndex + 1, pIndex + 3) if (indicator == "?=" || indicator == "?!") { // Look-ahead group pIndex += 3 @@ -427,7 +428,7 @@ private[regex] object IndicesBuilder { new LookAroundNode(isLookBehind = false, indicator, inner) } else if (indicator == "?<") { // Look-behind group, which must be ?<= or ? @@ -487,13 +488,13 @@ private[regex] object IndicesBuilder { val startIndex = pIndex pIndex = loop(startIndex + 1) - val regex = pattern.substring(startIndex, pIndex) + val regex = pattern.jsSubstring(startIndex, pIndex) new LeafRegexNode(regex) case _ => val start = pIndex pIndex += Character.charCount(dispatchCP) - new LeafRegexNode(pattern.substring(start, pIndex)) + new LeafRegexNode(pattern.jsSubstring(start, pIndex)) } if (baseNode ne null) { // null if we just completed an alternative @@ -505,7 +506,7 @@ private[regex] object IndicesBuilder { else pIndex += 1 - val repeater = pattern.substring(startIndex, pIndex) + val repeater = pattern.jsSubstring(startIndex, pIndex) sequence.push(new RepeatedNode(baseNode, repeater)) case '{' => @@ -514,7 +515,7 @@ private[regex] object IndicesBuilder { pIndex = pattern.indexOf("}", startIndex + 1) + 1 if (pattern.charAt(pIndex) == '?') // non-greedy mark pIndex += 1 - val repeater = pattern.substring(startIndex, pIndex) + val repeater = pattern.jsSubstring(startIndex, pIndex) sequence.push(new RepeatedNode(baseNode, repeater)) case _ => diff --git a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala index 51253c5744..cb9974f382 100644 --- a/javalib/src/main/scala/java/util/regex/PatternCompiler.scala +++ b/javalib/src/main/scala/java/util/regex/PatternCompiler.scala @@ -29,6 +29,7 @@ import java.lang.Utils._ import java.util.ScalaOps._ import scala.scalajs.js +import scala.scalajs.js.JSStringOps.enableJSStringOps import scala.scalajs.runtime.linkingInfo import scala.scalajs.LinkingInfo.ESVersion @@ -807,7 +808,7 @@ private final class PatternCompiler(private val pattern: String, private var fla val jsPattern = if (isLiteral) { literal(pattern) } else { - if (pattern.substring(pIndex, pIndex + 2) == "\\G") { + if (pattern.jsSubstring(pIndex, pIndex + 2) == "\\G") { sticky = true pIndex += 2 } @@ -1133,7 +1134,7 @@ private final class PatternCompiler(private val pattern: String, private var fla pIndex += 1 } - pattern.substring(startOfRepeater, pIndex) + pattern.jsSubstring(startOfRepeater, pIndex) } /** Builds a possessive quantifier, which is sugar for an atomic group over @@ -1262,7 +1263,7 @@ private final class PatternCompiler(private val pattern: String, private var fla // Boundary matchers case 'b' => - if (pattern.substring(pIndex, pIndex + 4) == "b{g}") { + if (pattern.jsSubstring(pIndex, pIndex + 4) == "b{g}") { parseError("\\b{g} is not supported") } else { /* Compile as is if both `UNICODE_CASE` and `UNICODE_CHARACTER_CLASS` are false. @@ -1345,11 +1346,11 @@ private final class PatternCompiler(private val pattern: String, private var fla // In most cases, one of the first two conditions is immediately false while (end != len && isDigit(pattern.charAt(end)) && - parseInt(pattern.substring(start, end + 1), 10) <= originalGroupCount) { + parseInt(pattern.jsSubstring(start, end + 1), 10) <= originalGroupCount) { end += 1 } - val groupString = pattern.substring(start, end) + val groupString = pattern.jsSubstring(start, end) val groupNumber = parseInt(groupString, 10) if (groupNumber > originalGroupCount) parseError(s"numbered capturing group <$groupNumber> does not exist") @@ -1379,10 +1380,10 @@ private final class PatternCompiler(private val pattern: String, private var fla val end = pattern.indexOf("\\E", start) if (end < 0) { pIndex = pattern.length() - literal(pattern.substring(start)) + literal(pattern.jsSubstring(start)) } else { pIndex = end + 2 - literal(pattern.substring(start, end)) + literal(pattern.jsSubstring(start, end)) } // Other @@ -1527,7 +1528,7 @@ private final class PatternCompiler(private val pattern: String, private var fla val lowStart = end + 2 val lowEnd = lowStart + 4 - if (isHighSurrogateCP(codeUnit) && pattern.substring(end, lowStart) == "\\u") { + if (isHighSurrogateCP(codeUnit) && pattern.jsSubstring(end, lowStart) == "\\u") { val low = parseHexCodePoint(lowStart, lowEnd, "Unicode") if (isLowSurrogateCP(low)) { pIndex = lowEnd @@ -1554,7 +1555,7 @@ private final class PatternCompiler(private val pattern: String, private var fla val cp = if (end - start > 6) Character.MAX_CODE_POINT + 1 - else parseInt(pattern.substring(start, end), 16) + else parseInt(pattern.jsSubstring(start, end), 16) if (cp > Character.MAX_CODE_POINT) parseError("Hexadecimal codepoint is too big") @@ -1604,9 +1605,9 @@ private final class PatternCompiler(private val pattern: String, private var fla if (innerEnd < 0) parseError("Unclosed character family") pIndex = innerEnd - pattern.substring(innerStart, innerEnd) + pattern.jsSubstring(innerStart, innerEnd) } else { - pattern.substring(start, start + 1) + pattern.jsSubstring(start, start + 1) } val result = if (!unicodeCharacterClass && dictContains(asciiPOSIXCharacterClasses, property)) { @@ -1631,7 +1632,7 @@ private final class PatternCompiler(private val pattern: String, private var fla // Error parseError(s"Unknown Unicode character class '$property'") } - CompiledCharClass.posP("sc=" + canonicalizeScriptName(property.substring(scriptPrefixLen))) + CompiledCharClass.posP("sc=" + canonicalizeScriptName(property.jsSubstring(scriptPrefixLen))) } } @@ -1796,7 +1797,7 @@ private final class PatternCompiler(private val pattern: String, private var fla if (c1 == ':' || c1 == '=' || c1 == '!') { // Non-capturing group or look-ahead pIndex = start + 3 - pattern.substring(start, start + 3) + compileInsideGroup() + ")" + pattern.jsSubstring(start, start + 3) + compileInsideGroup() + ")" } else if (c1 == '<') { if (start + 3 == len) parseError("Unclosed group") @@ -1820,7 +1821,7 @@ private final class PatternCompiler(private val pattern: String, private var fla parseError("Unknown look-behind group") requireES2018Features("Look-behind group") pIndex = start + 4 - pattern.substring(start, start + 4) + compileInsideGroup() + ")" + pattern.jsSubstring(start, start + 4) + compileInsideGroup() + ")" } } else if (c1 == '>') { // Atomic group @@ -1848,6 +1849,6 @@ private final class PatternCompiler(private val pattern: String, private var fla pIndex += 1 if (pIndex == len || pattern.charAt(pIndex) != '>') parseError("named capturing group is missing trailing '>'") - pattern.substring(start, pIndex) + pattern.jsSubstring(start, pIndex) } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index d5da6629d2..de5bfec6df 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 145210, - expectedFullLinkSizeWithoutClosure = 134353, - expectedFullLinkSizeWithClosure = 21671, + expectedFastLinkSize = 144813, + expectedFullLinkSizeWithoutClosure = 133956, + expectedFullLinkSizeWithClosure = 21669, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/FormatterTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/FormatterTest.scala index 3871f384bd..f22659c320 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/FormatterTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/FormatterTest.scala @@ -103,6 +103,8 @@ class FormatterTest { assertF("nul", "%.3" + conversion, null) assertF(" nul", "%5.3" + conversion, null) assertF("nul ", "%-5.3" + conversion, null) + assertF("null", "%.4" + conversion, null) + assertF("null", "%.10" + conversion, null) } if (acceptUpperCase) { @@ -190,6 +192,9 @@ class FormatterTest { assertF(" tru", "%8.3b", true) assertF("fal", "%.3b", null) assertF(" fal", "%8.3b", null) + assertF("true", "%.7b", true) + assertF("false", "%.7b", false) + assertF("false", "%.7b", null) expectFormatFlagsConversionMismatch('b', "#+ 0,(", true) expectFormatFlagsConversionMismatch('b', "#+ 0,(", null) @@ -203,6 +208,7 @@ class FormatterTest { assertF(" f1e2a3", "%8h", x) assertF("f1e2a", "%.5h", x) + assertF("f1e2a3", "%.10h", x) testWithNull('h', "") @@ -222,6 +228,7 @@ class FormatterTest { assertF("hel", "%.3s", "hello") assertF(" HEL", "%7.3S", "hello") + assertF("hello", "%.10s", "hello") testWithNull('s', "") From b1137d5bc6aa94b6449c02839dd9d35ed5f5de56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 8 Aug 2022 15:45:45 +0200 Subject: [PATCH 45/75] Introduce a CheckedBehavior for StringIndexOutOfBoundsException. Fundamentally, the primitive operation that causes language- mandated `StringIndexOutOfBoundsException`s is `String.charAt`. In order for the linker to recognize it and conditionally apply checked behavior semantics to it, we introduce a new IR `BinaryOp.String_charAt` for that operation. We use a compiler back-end hack to inject that IR node as the body of `String.charAt`. Using a dedicated primitive method in `scalajs.runtime`, like we did for `identityHashCode`, would not work so well here, as we want the binary operator to manipulate a *primitive* `StringType`, not a `ClassType(BoxedStringClass)`. For derived methods that are implemented on top of JS primitives, namely `codePointAt` and `substring`, we use a trick similar to what we did in `j.u.Arrays`: we introduce carefully-chosen no-op calls to `charAt`, with the only purpose of checking bounds, before delegating to the JS methods. Since `s.length()` is not known to be pure by the optimizer, we use clever tricks not to call it in the bounds checks of `substring`. --- .../org/scalajs/nscplugin/GenJSCode.scala | 8 +- .../src/main/scala/org/scalajs/ir/Names.scala | 4 + .../main/scala/org/scalajs/ir/Printers.scala | 6 + .../src/main/scala/org/scalajs/ir/Trees.scala | 5 + .../scala/org/scalajs/ir/PrintersTest.scala | 3 + .../src/main/scala/java/lang/_String.scala | 21 ++- .../scalajs/linker/interface/Semantics.scala | 24 ++- .../linker/backend/emitter/CoreJSLib.scala | 16 ++ .../linker/backend/emitter/Emitter.scala | 7 +- .../linker/backend/emitter/EmitterNames.scala | 1 + .../backend/emitter/FunctionEmitter.scala | 12 ++ .../scalajs/linker/checker/IRChecker.scala | 6 +- .../frontend/optimizer/OptimizerCore.scala | 32 +++- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- .../scala/tools/nsc/MainGenericRunner.scala | 7 +- project/BinaryIncompatibilities.scala | 2 + project/Build.scala | 10 +- .../scalajs/testsuite/utils/BuildInfo.scala | 1 + .../scalajs/testsuite/utils/Platform.scala | 3 + .../scalajs/testsuite/utils/Platform.scala | 1 + .../testsuite/javalib/lang/StringTest.scala | 154 ++++++++++++++++-- 21 files changed, 286 insertions(+), 43 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 8f08fb4a12..84a22a0d77 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2236,7 +2236,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) isJSFunctionDef(currentClassSym)) { val flags = js.MemberFlags.empty.withNamespace(namespace) val body = { - if (isImplClass(currentClassSym)) { + if (currentClassSym.get == HackedStringClass && methodName.name == charAtMethodName) { + // Hijack the body of String.charAt and replace it with a String_charAt binary op + js.BinaryOp(js.BinaryOp.String_charAt, genThis(), jsParams.head.ref) + } else if (isImplClass(currentClassSym)) { val thisParam = jsParams.head withScopedVars( thisLocalVarIdent := Some(thisParam.name) @@ -7059,6 +7062,9 @@ private object GenJSCode { private val ObjectArgConstructorName = MethodName.constructor(List(jstpe.ClassRef(ir.Names.ObjectClass))) + private val charAtMethodName = + MethodName("charAt", List(jstpe.IntRef), jstpe.CharRef) + private val thisOriginalName = OriginalName("this") private object BlockOrAlone { 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 d05ebc5ad7..1a564140e5 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Names.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Names.scala @@ -501,6 +501,10 @@ object Names { val ArrayIndexOutOfBoundsExceptionClass: ClassName = ClassName("java.lang.ArrayIndexOutOfBoundsException") + /** 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") 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 e5cfe2e510..8183c27558 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -411,6 +411,12 @@ object Printers { print(rhs) print(')') + case BinaryOp(BinaryOp.String_charAt, lhs, rhs) => + print(lhs) + print('[') + print(rhs) + print(']') + case BinaryOp(op, lhs, rhs) => import BinaryOp._ print('(') 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 b0eb1b5df3..016c9a5944 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -420,6 +420,9 @@ object Trees { final val Double_> = 56 final val Double_>= = 57 + // New in 1.11 + final val String_charAt = 58 + def resultTypeOf(op: Code): Type = (op: @switch) match { case === | !== | Boolean_== | Boolean_!= | Boolean_| | Boolean_& | @@ -439,6 +442,8 @@ object Trees { FloatType case Double_+ | Double_- | Double_* | Double_/ | Double_% => DoubleType + case String_charAt => + CharType } } 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 13972db5ed..795ed79d9d 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -538,6 +538,9 @@ class PrintersTest { BinaryOp(Double_>, ref("x", DoubleType), ref("y", DoubleType))) assertPrintEquals("(x >=[double] y)", BinaryOp(Double_>=, ref("x", DoubleType), ref("y", DoubleType))) + + assertPrintEquals("x[y]", + BinaryOp(String_charAt, ref("x", StringType), ref("y", IntType))) } @Test def printNewArray(): Unit = { diff --git a/javalib/src/main/scala/java/lang/_String.scala b/javalib/src/main/scala/java/lang/_String.scala index 8bbc49f136..675fedf353 100644 --- a/javalib/src/main/scala/java/lang/_String.scala +++ b/javalib/src/main/scala/java/lang/_String.scala @@ -48,10 +48,11 @@ final class _String private () // scalastyle:ignore @inline def charAt(index: Int): Char = - this.asInstanceOf[js.Dynamic].charCodeAt(index).asInstanceOf[Int].toChar + throw new Error("stub") // body replaced by the compiler back-end def codePointAt(index: Int): Int = { if (linkingInfo.esVersion >= ESVersion.ES2015) { + charAt(index) // bounds check this.asInstanceOf[js.Dynamic].codePointAt(index).asInstanceOf[Int] } else { val high = charAt(index) @@ -372,12 +373,26 @@ final class _String private () // scalastyle:ignore substring(beginIndex, endIndex) @inline - def substring(beginIndex: Int): String = + def substring(beginIndex: Int): String = { + // Bounds check (cleverly not using length() yet reporting errors in the appropriate cases) + if (beginIndex != 0) + charAt(beginIndex - 1) + thisString.jsSubstring(beginIndex) + } @inline - def substring(beginIndex: Int, endIndex: Int): String = + def substring(beginIndex: Int, endIndex: Int): String = { + // Bounds check (cleverly not using length() yet reporting errors in the appropriate cases) + if (beginIndex != 0) + charAt(beginIndex - 1) + if (endIndex != 0) + charAt(endIndex - 1) + if (endIndex < beginIndex) + charAt(-1) + thisString.jsSubstring(beginIndex, endIndex) + } def toCharArray(): Array[Char] = { val len = length() diff --git a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala index 6fbaa9b13b..3254902afa 100644 --- a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala +++ b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/Semantics.scala @@ -18,6 +18,7 @@ import Fingerprint.FingerprintBuilder final class Semantics private ( val asInstanceOfs: CheckedBehavior, val arrayIndexOutOfBounds: CheckedBehavior, + val stringIndexOutOfBounds: CheckedBehavior, val moduleInit: CheckedBehavior, val strictFloats: Boolean, val productionMode: Boolean, @@ -31,6 +32,9 @@ final class Semantics private ( def withArrayIndexOutOfBounds(behavior: CheckedBehavior): Semantics = copy(arrayIndexOutOfBounds = behavior) + def withStringIndexOutOfBounds(behavior: CheckedBehavior): Semantics = + copy(stringIndexOutOfBounds = behavior) + def withModuleInit(moduleInit: CheckedBehavior): Semantics = copy(moduleInit = moduleInit) @@ -53,6 +57,7 @@ final class Semantics private ( def optimized: Semantics = { copy(asInstanceOfs = this.asInstanceOfs.optimized, arrayIndexOutOfBounds = this.arrayIndexOutOfBounds.optimized, + stringIndexOutOfBounds = this.stringIndexOutOfBounds.optimized, moduleInit = this.moduleInit.optimized, productionMode = true) } @@ -61,6 +66,7 @@ final class Semantics private ( case that: Semantics => this.asInstanceOfs == that.asInstanceOfs && this.arrayIndexOutOfBounds == that.arrayIndexOutOfBounds && + this.stringIndexOutOfBounds == that.stringIndexOutOfBounds && this.moduleInit == that.moduleInit && this.strictFloats == that.strictFloats && this.productionMode == that.productionMode && @@ -74,26 +80,29 @@ final class Semantics private ( var acc = HashSeed acc = mix(acc, asInstanceOfs.##) acc = mix(acc, arrayIndexOutOfBounds.##) + acc = mix(acc, stringIndexOutOfBounds.##) acc = mix(acc, moduleInit.##) acc = mix(acc, strictFloats.##) acc = mix(acc, productionMode.##) acc = mixLast(acc, runtimeClassNameMapper.##) - finalizeHash(acc, 6) + finalizeHash(acc, 7) } override def toString(): String = { s"""Semantics( - | asInstanceOfs = $asInstanceOfs, - | arrayIndexOutOfBounds = $arrayIndexOutOfBounds, - | moduleInit = $moduleInit, - | strictFloats = $strictFloats, - | productionMode = $productionMode + | asInstanceOfs = $asInstanceOfs, + | arrayIndexOutOfBounds = $arrayIndexOutOfBounds, + | stringIndexOutOfBounds = $stringIndexOutOfBounds, + | moduleInit = $moduleInit, + | strictFloats = $strictFloats, + | productionMode = $productionMode |)""".stripMargin } private def copy( asInstanceOfs: CheckedBehavior = this.asInstanceOfs, arrayIndexOutOfBounds: CheckedBehavior = this.arrayIndexOutOfBounds, + stringIndexOutOfBounds: CheckedBehavior = this.stringIndexOutOfBounds, moduleInit: CheckedBehavior = this.moduleInit, strictFloats: Boolean = this.strictFloats, productionMode: Boolean = this.productionMode, @@ -102,6 +111,7 @@ final class Semantics private ( new Semantics( asInstanceOfs = asInstanceOfs, arrayIndexOutOfBounds = arrayIndexOutOfBounds, + stringIndexOutOfBounds = stringIndexOutOfBounds, moduleInit = moduleInit, strictFloats = strictFloats, productionMode = productionMode, @@ -217,6 +227,7 @@ object Semantics { new FingerprintBuilder("Semantics") .addField("asInstanceOfs", semantics.asInstanceOfs) .addField("arrayIndexOutOfBounds", semantics.arrayIndexOutOfBounds) + .addField("stringIndexOutOfBounds", semantics.stringIndexOutOfBounds) .addField("moduleInit", semantics.moduleInit) .addField("strictFloats", semantics.strictFloats) .addField("productionMode", semantics.productionMode) @@ -228,6 +239,7 @@ object Semantics { val Defaults: Semantics = new Semantics( asInstanceOfs = Fatal, arrayIndexOutOfBounds = Fatal, + stringIndexOutOfBounds = Fatal, moduleInit = Unchecked, strictFloats = true, productionMode = false, diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index 725e158d04..57f0f83d1e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -892,6 +892,22 @@ private[emitter] object CoreJSLib { Return(If(x > 2147483647, 2147483647, If(x < -2147483648, -2147483648, x | 0))) }, + condTree(semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked)( + defineFunction2("charAt") { (s, i) => + val r = varRef("r") + + val throwStringIndexOutOfBoundsException = { + Throw(maybeWrapInUBE(semantics.stringIndexOutOfBounds, + genScalaClassNew(StringIndexOutOfBoundsExceptionClass, IntArgConstructorName, i))) + } + + Block( + const(r, Apply(genIdentBracketSelect(s, "charCodeAt"), List(i))), + If(r !== r, throwStringIndexOutOfBoundsException, Return(r)) + ) + } + ), + condTree(allowBigIntsForLongs)(Block( defineFunction2("longDiv") { (x, y) => If(y === bigInt(0), throwDivByZero, { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index fbd3c06af2..714058bae7 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -828,7 +828,12 @@ object Emitter { StringArgConstructorName) }, - cond(asInstanceOfs == Fatal || arrayIndexOutOfBounds == Fatal) { + cond(stringIndexOutOfBounds != Unchecked) { + instantiateClass(StringIndexOutOfBoundsExceptionClass, + IntArgConstructorName) + }, + + cond(asInstanceOfs == Fatal || arrayIndexOutOfBounds == Fatal || stringIndexOutOfBounds == Fatal) { instantiateClass(UndefinedBehaviorErrorClass, ThrowableArgConsructorName) }, diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala index d2c031f805..17c03b75dc 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala @@ -31,6 +31,7 @@ private[emitter] object EmitterNames { // Method names val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) + val IntArgConstructorName = MethodName.constructor(List(IntRef)) val StringArgConstructorName = MethodName.constructor(List(ClassRef(BoxedStringClass))) val ThrowableArgConsructorName = MethodName.constructor(List(ClassRef(ThrowableClass))) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index c82ef8e9a5..f7835e694c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -1271,6 +1271,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case _ => false } + // String_charAt preserves pureness iff the semantics for stringIndexOutOfBounds are unchecked + case BinaryOp(BinaryOp.String_charAt, lhs, rhs) => + (allowSideEffects || semantics.stringIndexOutOfBounds == Unchecked) && test(lhs) && test(rhs) + // Expressions preserving pureness case Block(trees) => trees forall test case If(cond, thenp, elsep) => test(cond) && test(thenp) && test(elsep) @@ -2621,6 +2625,14 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Boolean_| => !(!js.BinaryOp(JSBinaryOp.|, newLhs, newRhs)) case Boolean_& => !(!js.BinaryOp(JSBinaryOp.&, newLhs, newRhs)) + + case String_charAt => + semantics.stringIndexOutOfBounds match { + case CheckedBehavior.Compliant | CheckedBehavior.Fatal => + genCallHelper("charAt", newLhs, newRhs) + case CheckedBehavior.Unchecked => + js.Apply(genIdentBracketSelect(newLhs, "charCodeAt"), List(newRhs)) + } } case NewArray(typeRef, lengths) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 096ce7e83f..5d146dea6b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -513,10 +513,12 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { Double_== | Double_!= | Double_< | Double_<= | Double_> | Double_>= => DoubleType + case String_charAt => + StringType } val expectedRhsType = (op: @switch) match { - case Long_<< | Long_>>> | Long_>> => IntType - case _ => expectedLhsType + case Long_<< | Long_>>> | Long_>> | String_charAt => IntType + case _ => expectedLhsType } typecheckExpect(lhs, env, expectedLhsType) typecheckExpect(rhs, env, expectedRhsType) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 6a4d218dd6..910bbef9a1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -47,6 +47,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { val myself: MethodID + private def semantics: Semantics = config.coreSpec.semantics + // Uncomment and adapt to print debug messages only during one method //lazy val debugThisMethod: Boolean = // myself.toString() == "java.lang.FloatingPointBits$.numberHashCode;D;I" @@ -1440,15 +1442,15 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { finishTransformStat(lhs) case PreTransBinaryOp(op, lhs, rhs) => - // Here we need to preserve the side-effects of integer division/modulo + // Here we need to preserve the side-effects of integer division/modulo and String_charAt import BinaryOp._ - val newLhs = finishTransformStat(lhs) + def newLhs = finishTransformStat(lhs) def finishNoSideEffects: Tree = Block(newLhs, finishTransformStat(rhs))(stat.pos) - op match { + (op: @switch) match { case Int_/ | Int_% => rhs match { case PreTransLit(IntLiteral(r)) if r != 0 => @@ -1465,6 +1467,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { Block(newLhs, BinaryOp(op, LongLiteral(0L)(stat.pos), finishTransformExpr(rhs))(stat.pos))(stat.pos) } + case String_charAt if semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked => + finishTransformExpr(stat) case _ => finishNoSideEffects } @@ -1558,11 +1562,12 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } case BinaryOp(op, lhs, rhs) => - // Here we need to preserve the side-effects of integer division/modulo + // Here we need to preserve the side-effects of integer division/modulo and String_charAt import BinaryOp._ implicit val pos = stat.pos - val newLhs = keepOnlySideEffects(lhs) + + def newLhs = keepOnlySideEffects(lhs) def finishNoSideEffects: Tree = Block(newLhs, keepOnlySideEffects(rhs)) @@ -1582,6 +1587,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case _ => Block(newLhs, BinaryOp(op, LongLiteral(0L), rhs)) } + case String_charAt if semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked => + stat case _ => finishNoSideEffects } @@ -3249,6 +3256,10 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case BooleanLiteral(value) => value } + @inline def string(lit: Literal): String = (lit: @unchecked) match { + case StringLiteral(value) => value + } + (op: @switch) match { case === => BooleanLiteral(literal_===(lhs, rhs, isJSStrictEq = false)) case !== => BooleanLiteral(!literal_===(lhs, rhs, isJSStrictEq = false)) @@ -3320,6 +3331,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Boolean_!= => BooleanLiteral(boolean(lhs) != boolean(rhs)) case Boolean_| => BooleanLiteral(boolean(lhs) | boolean(rhs)) case Boolean_& => BooleanLiteral(boolean(lhs) & boolean(rhs)) + + case String_charAt => CharLiteral(string(lhs).charAt(int(rhs))) } } @@ -3393,6 +3406,13 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case LongLiteral(0) => nonConstant() case _ => constant(lhsLit, rhsLit) } + case String_charAt => + (lhsLit, rhsLit) match { + case (StringLiteral(lhsStr), IntLiteral(rhsInt)) if rhsInt < 0 || rhsInt >= lhsStr.length() => + nonConstant() + case _ => + constant(lhsLit, rhsLit) + } case _ => constant(lhsLit, rhsLit) } @@ -4225,7 +4245,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { implicit pos: Position): Tree = { // !!! Must be in sync with scala.scalajs.runtime.LinkingInfo - import config.coreSpec._ + import config.coreSpec.esFeatures (qualifier, item) match { case (JSLinkingInfo(), StringLiteral("productionMode")) => diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index de5bfec6df..7c91268456 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 144813, - expectedFullLinkSizeWithoutClosure = 133956, - expectedFullLinkSizeWithClosure = 21669, + expectedFastLinkSize = 145003, + expectedFullLinkSizeWithoutClosure = 133198, + expectedFullLinkSizeWithClosure = 21338, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/partest/src/main/scala/scala/tools/nsc/MainGenericRunner.scala b/partest/src/main/scala/scala/tools/nsc/MainGenericRunner.scala index 7aea515d4d..fc8f1a06ec 100644 --- a/partest/src/main/scala/scala/tools/nsc/MainGenericRunner.scala +++ b/partest/src/main/scala/scala/tools/nsc/MainGenericRunner.scala @@ -60,9 +60,10 @@ class MainGenericRunner { compliantSems.foldLeft(Semantics.Defaults) { (prev, compliantSem) => compliantSem match { - case "asInstanceOfs" => prev.withAsInstanceOfs(Compliant) - case "arrayIndexOutOfBounds" => prev.withArrayIndexOutOfBounds(Compliant) - case "moduleInit" => prev.withModuleInit(Compliant) + case "asInstanceOfs" => prev.withAsInstanceOfs(Compliant) + case "arrayIndexOutOfBounds" => prev.withArrayIndexOutOfBounds(Compliant) + case "stringIndexOutOfBounds" => prev.withStringIndexOutOfBounds(Compliant) + case "moduleInit" => prev.withModuleInit(Compliant) } } } diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 7f3b4db846..7ac0e840d5 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -15,6 +15,8 @@ object BinaryIncompatibilities { ) val LinkerInterface = Seq( + // private, not an issue + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.interface.Semantics.this"), ) val SbtPlugin = Seq( diff --git a/project/Build.scala b/project/Build.scala index f058e29137..90c923a9d9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -45,6 +45,7 @@ object ExposedValues extends AutoPlugin { semantics .withAsInstanceOfs(CheckedBehavior.Compliant) .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) + .withStringIndexOutOfBounds(CheckedBehavior.Compliant) .withModuleInit(CheckedBehavior.Compliant) } } @@ -1712,15 +1713,15 @@ object Build { scalaVersion.value match { case Default2_11ScalaVersion => Some(ExpectedSizes( - fastLink = 515000 to 516000, + fastLink = 516500 to 517500, fullLink = 107000 to 108000, - fastLinkGz = 65000 to 66000, + fastLinkGz = 65500 to 66500, fullLinkGz = 28000 to 29000, )) case Default2_12ScalaVersion => Some(ExpectedSizes( - fastLink = 777000 to 778000, + fastLink = 778000 to 779000, fullLink = 148000 to 149000, fastLinkGz = 90000 to 91000, fullLinkGz = 36000 to 37000, @@ -1731,7 +1732,7 @@ object Build { fastLink = 727000 to 728000, fullLink = 155000 to 156000, fastLinkGz = 91000 to 92000, - fullLinkGz = 40000 to 41000, + fullLinkGz = 39000 to 40000, )) case _ => @@ -2011,6 +2012,7 @@ object Build { "isFullOpt" -> (stage == Stage.FullOpt), "compliantAsInstanceOfs" -> (sems.asInstanceOfs == CheckedBehavior.Compliant), "compliantArrayIndexOutOfBounds" -> (sems.arrayIndexOutOfBounds == CheckedBehavior.Compliant), + "compliantStringIndexOutOfBounds" -> (sems.stringIndexOutOfBounds == CheckedBehavior.Compliant), "compliantModuleInit" -> (sems.moduleInit == CheckedBehavior.Compliant), "strictFloats" -> sems.strictFloats, "productionMode" -> sems.productionMode, diff --git a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala index faf9922631..11c8085483 100644 --- a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala +++ b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala @@ -25,6 +25,7 @@ private[utils] object BuildInfo { final val isFullOpt = false final val compliantAsInstanceOfs = false final val compliantArrayIndexOutOfBounds = false + final val compliantStringIndexOutOfBounds = false final val compliantModuleInit = false final val strictFloats = false final val productionMode = false diff --git a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index daed18e54e..4e1adc3cd6 100644 --- a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -72,6 +72,9 @@ object Platform { def hasCompliantArrayIndexOutOfBounds: Boolean = BuildInfo.compliantArrayIndexOutOfBounds + def hasCompliantStringIndexOutOfBounds: Boolean = + BuildInfo.compliantStringIndexOutOfBounds + def hasCompliantModuleInit: Boolean = BuildInfo.compliantModuleInit def hasDirectBuffers: Boolean = typedArrays diff --git a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index 7e5537dc1d..d6575fc33c 100644 --- a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -40,6 +40,7 @@ object Platform { def hasCompliantAsInstanceOfs: Boolean = true def hasCompliantArrayIndexOutOfBounds: Boolean = true + def hasCompliantStringIndexOutOfBounds: Boolean = true def hasCompliantModule: Boolean = true def hasDirectBuffers: Boolean = true def hasStrictFloats: Boolean = true diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/StringTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/StringTest.scala index 43f1f073f5..fb88a1620b 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/StringTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/StringTest.scala @@ -16,6 +16,7 @@ import java.nio.charset.Charset import org.junit.Test import org.junit.Assert._ +import org.junit.Assume._ import org.scalajs.testsuite.utils.AssertThrows.assertThrows import org.scalajs.testsuite.utils.Platform._ @@ -144,8 +145,42 @@ class StringTest { } @Test def charAt(): Unit = { - assertEquals('.', "Scala.js".charAt(5)) - assertNotEquals("Scala.js".charAt(6), '.') + @noinline def testNoInline(expected: Char, s: String, i: Int): Unit = + assertEquals(expected, s.charAt(i)) + + @inline def test(expected: Char, s: String, i: Int): Unit = { + testNoInline(expected, s, i) + assertEquals(expected, s.charAt(i)) + } + + test('S', "Scala.js", 0) + test('.', "Scala.js", 5) + test('s', "Scala.js", 7) + test('o', "foo", 1) + } + + @Test def charAtIndexOutOfBounds(): Unit = { + assumeTrue("Assuming compliant StringIndexOutOfBounds", + hasCompliantStringIndexOutOfBounds) + + def test(s: String, i: Int): Unit = { + val e = assertThrows(classOf[StringIndexOutOfBoundsException], s.charAt(i)) + assertTrue(e.getMessage(), e.getMessage().contains(i.toString())) + } + + test("foo", -1) + test("foo", -10000) + test("foo", Int.MinValue) + test("foo", 3) + test("foo", 10000) + test("foo", Int.MaxValue) + + test("", -1) + test("", 0) + test("", 1) + + // Test non-constant-folding + assertThrows(classOf[StringIndexOutOfBoundsException], "foo".charAt(4)) } @Test def codePointAt(): Unit = { @@ -165,13 +200,18 @@ class StringTest { // Lone low surrogates assertEquals(0xDF06, "\uDF06abc".codePointAt(0)) assertEquals(0xD834, "abc\uD834".codePointAt(3)) + } - if (executingInJVM) { - assertThrows(classOf[IndexOutOfBoundsException], - "abc\ud834\udf06def".codePointAt(-1)) - assertThrows(classOf[IndexOutOfBoundsException], - "abc\ud834\udf06def".codePointAt(15)) - } + @Test def codePointAtIndexOutOfBounds(): Unit = { + assumeTrue("Assuming compliant StringIndexOutOfBounds", + hasCompliantStringIndexOutOfBounds) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "abc\ud834\udf06def".codePointAt(-1)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "abc\ud834\udf06def".codePointAt(8)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "abc\ud834\udf06def".codePointAt(15)) } @Test def codePointBefore(): Unit = { @@ -179,17 +219,23 @@ class StringTest { assertEquals(0x1d306, "abc\ud834\udf06def".codePointBefore(5)) assertEquals(0xd834, "abc\ud834\udf06def".codePointBefore(4)) assertEquals(0x64, "abc\ud834\udf06def".codePointBefore(6)) + assertEquals('f'.toInt, "abc\ud834\udf06def".codePointBefore(8)) assertEquals(0x1d306, "\ud834\udf06def".codePointBefore(2)) assertEquals(0xd834, "\ud834\udf06def".codePointBefore(1)) assertEquals(0xd834, "\ud834abc".codePointBefore(1)) assertEquals(0xdf06, "\udf06abc".codePointBefore(1)) + } - if (executingInJVM) { - assertThrows(classOf[IndexOutOfBoundsException], - "abc\ud834\udf06def".codePointBefore(0)) - assertThrows(classOf[IndexOutOfBoundsException], - "abc\ud834\udf06def".codePointBefore(15)) - } + @Test def codePointBeforeIndexOutOfBounds(): Unit = { + assumeTrue("Assuming compliant StringIndexOutOfBounds", + hasCompliantStringIndexOutOfBounds) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "abc\ud834\udf06def".codePointBefore(0)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "abc\ud834\udf06def".codePointBefore(9)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "abc\ud834\udf06def".codePointBefore(15)) } @Test def codePointCount(): Unit = { @@ -255,6 +301,64 @@ class StringTest { assertThrows(classOf[IndexOutOfBoundsException], s.offsetByCodePoints(30, 2)) } + @Test def substringBegin(): Unit = { + assertEquals("", "".substring(0)) + assertEquals("", "foo".substring(3)) + assertEquals("", "hello".substring(5)) + assertEquals("lo", "hello".substring(3)) + assertEquals("baz", "foo bar baz".substring(8)) + assertEquals("foo bar baz", "foo bar baz".substring(0)) + } + + @Test def substringBeginIndexOutOfBounds(): Unit = { + assumeTrue("Assuming compliant StringIndexOutOfBounds", + hasCompliantStringIndexOutOfBounds) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(-1)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(4)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(15)) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "".substring(-1)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "".substring(1)) + } + + @Test def substringBeginEnd(): Unit = { + assertEquals("", "".substring(0, 0)) + assertEquals("", "foo".substring(3, 3)) + assertEquals("", "hello".substring(3, 3)) + assertEquals("lo", "hello".substring(3, 5)) + assertEquals("bar", "foo bar baz".substring(4, 7)) + assertEquals("foo bar baz", "foo bar baz".substring(0, 11)) + assertEquals("foo bar", "foo bar baz".substring(0, 7)) + } + + @Test def substringBeginEndIndexOutOfBounds(): Unit = { + assumeTrue("Assuming compliant StringIndexOutOfBounds", + hasCompliantStringIndexOutOfBounds) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(-1, 1)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(4, 4)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(1, 4)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(-1, 4)) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".substring(2, 1)) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "".substring(-1, -1)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "".substring(1, 1)) + } + @Test def subSequence(): Unit = { assertEquals("Scala", "Scala.js".subSequence(0, 5)) assertEquals("js", "Scala.js".subSequence(6, 8)) @@ -262,6 +366,28 @@ class StringTest { assertEquals("", "Scala.js".subSequence(3, 3)) } + @Test def subSequenceIndexOutOfBounds(): Unit = { + assumeTrue("Assuming compliant StringIndexOutOfBounds", + hasCompliantStringIndexOutOfBounds) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".subSequence(-1, 1)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".subSequence(4, 4)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".subSequence(1, 4)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".subSequence(-1, 4)) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "foo".subSequence(2, 1)) + + assertThrows(classOf[StringIndexOutOfBoundsException], + "".subSequence(-1, -1)) + assertThrows(classOf[StringIndexOutOfBoundsException], + "".subSequence(1, 1)) + } + @Test def replace(): Unit = { assertEquals("Scala", "Scala.js".replace(".js", "")) assertEquals("Scala.js", "Scala.js".replace("JS", "")) From 7fbda3e9cb1e49487a6885bd1bbde90cb8fb4144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 9 Aug 2022 19:23:59 +0200 Subject: [PATCH 46/75] Introduce a UnaryOp for String.length. This is done mostly for consistency with String.charAt. Together with `String_+`, those two operations are the only primitives required to implement any manipulation on strings, so they make a coherent whole. In practice, this allows the optimizer to reason about that operation, in terms of constant-folding and purity. With that, we need not be afraid of using `length()` in bounds checks, which allows them to be clearer and more efficient. --- .../org/scalajs/nscplugin/GenJSCode.scala | 17 ++++++++++--- .../main/scala/org/scalajs/ir/Printers.scala | 4 ++++ .../src/main/scala/org/scalajs/ir/Trees.scala | 5 +++- .../scala/org/scalajs/ir/PrintersTest.scala | 2 ++ .../src/main/scala/java/lang/_String.scala | 24 +++++++++---------- .../backend/emitter/FunctionEmitter.scala | 4 ++++ .../scalajs/linker/checker/IRChecker.scala | 2 ++ .../frontend/optimizer/OptimizerCore.scala | 10 ++++++++ 8 files changed, 52 insertions(+), 16 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 84a22a0d77..3e16028af5 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2236,9 +2236,18 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) isJSFunctionDef(currentClassSym)) { val flags = js.MemberFlags.empty.withNamespace(namespace) val body = { - if (currentClassSym.get == HackedStringClass && methodName.name == charAtMethodName) { - // Hijack the body of String.charAt and replace it with a String_charAt binary op - js.BinaryOp(js.BinaryOp.String_charAt, genThis(), 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` => + js.UnaryOp(js.UnaryOp.String_length, genThis()) + case `charAtMethodName` => + js.BinaryOp(js.BinaryOp.String_charAt, genThis(), jsParams.head.ref) + case _ => + genBody() + } } else if (isImplClass(currentClassSym)) { val thisParam = jsParams.head withScopedVars( @@ -7062,6 +7071,8 @@ private object GenJSCode { 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) 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 8183c27558..0f1beb08b4 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -356,6 +356,10 @@ object Printers { print(method) printArgs(args) + case UnaryOp(UnaryOp.String_length, lhs) => + print(lhs) + print(".length") + case UnaryOp(op, lhs) => import UnaryOp._ print('(') 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 016c9a5944..b7183fa6c7 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -320,6 +320,9 @@ object Trees { // Long -> Float (neither widening nor narrowing), introduced in 1.6 final val LongToFloat = 16 + // String.length, introduced in 1.11 + final val String_length = 17 + def resultTypeOf(op: Code): Type = (op: @switch) match { case Boolean_! => BooleanType @@ -329,7 +332,7 @@ object Trees { ByteType case IntToShort => ShortType - case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt => + case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | String_length => IntType case IntToLong | DoubleToLong => LongType 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 795ed79d9d..397706d934 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -396,6 +396,8 @@ class PrintersTest { assertPrintEquals("((long)x)", UnaryOp(DoubleToLong, ref("x", DoubleType))) assertPrintEquals("((float)x)", UnaryOp(LongToFloat, ref("x", LongType))) + + assertPrintEquals("x.length", UnaryOp(String_length, ref("x", StringType))) } @Test def printPseudoUnaryOp(): Unit = { diff --git a/javalib/src/main/scala/java/lang/_String.scala b/javalib/src/main/scala/java/lang/_String.scala index 675fedf353..1daea18ac3 100644 --- a/javalib/src/main/scala/java/lang/_String.scala +++ b/javalib/src/main/scala/java/lang/_String.scala @@ -46,6 +46,10 @@ final class _String private () // scalastyle:ignore private def thisString: String = this.asInstanceOf[String] + @inline + def length(): Int = + throw new Error("stub") // body replaced by the compiler back-end + @inline def charAt(index: Int): Char = throw new Error("stub") // body replaced by the compiler back-end @@ -278,10 +282,6 @@ final class _String private () // scalastyle:ignore if (fromIndex < 0) -1 else thisString.jsLastIndexOf(str, fromIndex) - @inline - def length(): Int = - this.asInstanceOf[js.Dynamic].length.asInstanceOf[Int] - @inline def matches(regex: String): scala.Boolean = Pattern.matches(regex, thisString) @@ -374,20 +374,20 @@ final class _String private () // scalastyle:ignore @inline def substring(beginIndex: Int): String = { - // Bounds check (cleverly not using length() yet reporting errors in the appropriate cases) - if (beginIndex != 0) - charAt(beginIndex - 1) + // Bounds check + if (beginIndex < 0 || beginIndex > length()) + charAt(beginIndex) thisString.jsSubstring(beginIndex) } @inline def substring(beginIndex: Int, endIndex: Int): String = { - // Bounds check (cleverly not using length() yet reporting errors in the appropriate cases) - if (beginIndex != 0) - charAt(beginIndex - 1) - if (endIndex != 0) - charAt(endIndex - 1) + // Bounds check + if (beginIndex < 0) + charAt(beginIndex) + if (endIndex > length()) + charAt(endIndex) if (endIndex < beginIndex) charAt(-1) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index f7835e694c..4d40011643 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -2378,6 +2378,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genCallHelper("longToFloat", newLhs) else genLongMethodApply(newLhs, LongImpl.toFloat) + + // String.length + case String_length => + genIdentBracketSelect(newLhs, "length") } case BinaryOp(op, lhs, rhs) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 5d146dea6b..90a160e518 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -489,6 +489,8 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { FloatType case DoubleToInt | DoubleToFloat | DoubleToLong => DoubleType + case String_length => + StringType } typecheckExpect(lhs, env, expectedArgType) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 910bbef9a1..5bdd9a8681 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -3172,6 +3172,16 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { default } + // String.length + + case String_length => + arg match { + case PreTransLit(StringLiteral(s)) => + PreTransLit(IntLiteral(s.length())) + case _ => + default + } + case _ => default } From 22cd54ecd9c349fb168c0393df8c2de0b335b393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 21 Aug 2022 22:44:02 +0200 Subject: [PATCH 47/75] Refactor: Handle constant and non-constant cases in foldBinaryOp. Previously, we first dispatched between two methods depending on whether both arguments were literals or not. Although it avoided some boilerplate, the fact that error cases had to be handled away from non-error cases made it difficult to reason about correctness. We now deal with all the constant cases in the same big switch as everything else. --- The produced code is unchanged, except for `longLit / 0L` and `longLit % 0L` where `longLit` is a valid `Int`. The code is now a bit less efficient as it performs a Long division/remainder, instead of an Int operation. This is fine because it is an error case that systematically throws anyway. Reproducing exactly the same code as before would have required more complicated logic, which was not worth it. --- .../frontend/optimizer/OptimizerCore.scala | 526 ++++++++++-------- 1 file changed, 301 insertions(+), 225 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 5bdd9a8681..5d8207bccb 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -3242,110 +3242,6 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } } - private def constantFoldBinaryOp_except_String_+(op: BinaryOp.Code, - lhs: Literal, rhs: Literal)(implicit pos: Position): Literal = { - import BinaryOp._ - - @inline def int(lit: Literal): Int = (lit: @unchecked) match { - case IntLiteral(value) => value - } - - @inline def long(lit: Literal): Long = (lit: @unchecked) match { - case LongLiteral(value) => value - } - - @inline def float(lit: Literal): Float = (lit: @unchecked) match { - case FloatLiteral(value) => value - } - - @inline def double(lit: Literal): Double = (lit: @unchecked) match { - case DoubleLiteral(value) => value - } - - @inline def boolean(lit: Literal): Boolean = (lit: @unchecked) match { - case BooleanLiteral(value) => value - } - - @inline def string(lit: Literal): String = (lit: @unchecked) match { - case StringLiteral(value) => value - } - - (op: @switch) match { - case === => BooleanLiteral(literal_===(lhs, rhs, isJSStrictEq = false)) - case !== => BooleanLiteral(!literal_===(lhs, rhs, isJSStrictEq = false)) - - case String_+ => - throw new IllegalArgumentException( - "constFoldBinaryOp_except_String_+ must not be called for String_+") - - case Int_+ => IntLiteral(int(lhs) + int(rhs)) - case Int_- => IntLiteral(int(lhs) - int(rhs)) - case Int_* => IntLiteral(int(lhs) * int(rhs)) - case Int_/ => IntLiteral(int(lhs) / int(rhs)) - case Int_% => IntLiteral(int(lhs) % int(rhs)) - - case Int_| => IntLiteral(int(lhs) | int(rhs)) - case Int_& => IntLiteral(int(lhs) & int(rhs)) - case Int_^ => IntLiteral(int(lhs) ^ int(rhs)) - case Int_<< => IntLiteral(int(lhs) << int(rhs)) - case Int_>>> => IntLiteral(int(lhs) >>> int(rhs)) - case Int_>> => IntLiteral(int(lhs) >> int(rhs)) - - case Int_== => BooleanLiteral(int(lhs) == int(rhs)) - case Int_!= => BooleanLiteral(int(lhs) != int(rhs)) - case Int_< => BooleanLiteral(int(lhs) < int(rhs)) - case Int_<= => BooleanLiteral(int(lhs) <= int(rhs)) - case Int_> => BooleanLiteral(int(lhs) > int(rhs)) - case Int_>= => BooleanLiteral(int(lhs) >= int(rhs)) - - case Long_+ => LongLiteral(long(lhs) + long(rhs)) - case Long_- => LongLiteral(long(lhs) - long(rhs)) - case Long_* => LongLiteral(long(lhs) * long(rhs)) - case Long_/ => LongLiteral(long(lhs) / long(rhs)) - case Long_% => LongLiteral(long(lhs) % long(rhs)) - - case Long_| => LongLiteral(long(lhs) | long(rhs)) - case Long_& => LongLiteral(long(lhs) & long(rhs)) - case Long_^ => LongLiteral(long(lhs) ^ long(rhs)) - case Long_<< => LongLiteral(long(lhs) << int(rhs)) - case Long_>>> => LongLiteral(long(lhs) >>> int(rhs)) - case Long_>> => LongLiteral(long(lhs) >> int(rhs)) - - case Long_== => BooleanLiteral(long(lhs) == long(rhs)) - case Long_!= => BooleanLiteral(long(lhs) != long(rhs)) - case Long_< => BooleanLiteral(long(lhs) < long(rhs)) - case Long_<= => BooleanLiteral(long(lhs) <= long(rhs)) - case Long_> => BooleanLiteral(long(lhs) > long(rhs)) - case Long_>= => BooleanLiteral(long(lhs) >= long(rhs)) - - case Float_+ => FloatLiteral(float(lhs) + float(rhs)) - case Float_- => FloatLiteral(float(lhs) - float(rhs)) - case Float_* => FloatLiteral(float(lhs) * float(rhs)) - case Float_/ => FloatLiteral(float(lhs) / float(rhs)) - case Float_% => FloatLiteral(float(lhs) % float(rhs)) - - case Double_+ => DoubleLiteral(double(lhs) + double(rhs)) - case Double_- => DoubleLiteral(double(lhs) - double(rhs)) - case Double_* => DoubleLiteral(double(lhs) * double(rhs)) - case Double_/ => DoubleLiteral(double(lhs) / double(rhs)) - case Double_% => DoubleLiteral(double(lhs) % double(rhs)) - - case Double_== => BooleanLiteral(double(lhs) == double(rhs)) - case Double_!= => BooleanLiteral(double(lhs) != double(rhs)) - case Double_< => BooleanLiteral(double(lhs) < double(rhs)) - case Double_<= => BooleanLiteral(double(lhs) <= double(rhs)) - case Double_> => BooleanLiteral(double(lhs) > double(rhs)) - case Double_>= => BooleanLiteral(double(lhs) >= double(rhs)) - - case Boolean_== => BooleanLiteral(boolean(lhs) == boolean(rhs)) - case Boolean_!= => BooleanLiteral(boolean(lhs) != boolean(rhs)) - case Boolean_| => BooleanLiteral(boolean(lhs) | boolean(rhs)) - case Boolean_& => BooleanLiteral(boolean(lhs) & boolean(rhs)) - - case String_charAt => CharLiteral(string(lhs).charAt(int(rhs))) - } - } - /** Translate literals to their Scala.js String representation. */ private def foldToStringForString_+(preTrans: PreTransform)( implicit pos: Position): PreTransform = preTrans match { @@ -3391,47 +3287,6 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } } - private def foldBinaryOp(op: BinaryOp.Code, lhs: PreTransform, - rhs: PreTransform)( - implicit pos: Position): PreTransform = { - import BinaryOp._ - - def constant(lhsLit: Literal, rhsLit: Literal): PreTransform = - PreTransLit(constantFoldBinaryOp_except_String_+(op, lhsLit, rhsLit)) - - def nonConstant(): PreTransform = foldBinaryOpNonConstant(op, lhs, rhs) - - (lhs, rhs) match { - case (PreTransLit(lhsLit), PreTransLit(rhsLit)) => - op match { - case String_+ => - nonConstant() - case Int_/ | Int_% => - rhsLit match { - case IntLiteral(0) => nonConstant() - case _ => constant(lhsLit, rhsLit) - } - case Long_/ | Long_% => - rhsLit match { - case LongLiteral(0) => nonConstant() - case _ => constant(lhsLit, rhsLit) - } - case String_charAt => - (lhsLit, rhsLit) match { - case (StringLiteral(lhsStr), IntLiteral(rhsInt)) if rhsInt < 0 || rhsInt >= lhsStr.length() => - nonConstant() - case _ => - constant(lhsLit, rhsLit) - } - case _ => - constant(lhsLit, rhsLit) - } - - case _ => - nonConstant() - } - } - private val MaybeHijackedPrimNumberClasses = { /* In theory, we could figure out the ancestors from the global knowledge, * but that would be overkill. @@ -3442,14 +3297,27 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { ClassName("java.lang.Number")) } - private def foldBinaryOpNonConstant(op: BinaryOp.Code, lhs: PreTransform, + private def foldBinaryOp(op: BinaryOp.Code, lhs: PreTransform, rhs: PreTransform)( implicit pos: Position): PreTransform = { import BinaryOp._ - @inline def default = + @inline def default: PreTransform = PreTransBinaryOp(op, lhs, rhs) + @inline def booleanLit(value: Boolean): PreTransform = + PreTransLit(BooleanLiteral(value)) + @inline def charLit(value: Char): PreTransform = + PreTransLit(CharLiteral(value)) + @inline def intLit(value: Int): PreTransform = + PreTransLit(IntLiteral(value)) + @inline def longLit(value: Long): PreTransform = + PreTransLit(LongLiteral(value)) + @inline def floatLit(value: Float): PreTransform = + PreTransLit(FloatLiteral(value)) + @inline def doubleLit(value: Double): PreTransform = + PreTransLit(DoubleLiteral(value)) + (op: @switch) match { case === | !== => // Try to optimize as a primitive JS strict equality @@ -3480,24 +3348,25 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { false } - val lhsTpe = lhs.tpe - val rhsTpe = rhs.tpe - val canOptimizeAsJSStrictEq = ( + def canOptimizeAsJSStrictEq(lhsTpe: RefinedType, rhsTpe: RefinedType): Boolean = ( !canBePrimitiveNum(lhsTpe) || !canBePrimitiveNum(rhsTpe) || (isWhole(lhsTpe) && isWhole(rhsTpe)) ) - if (canOptimizeAsJSStrictEq) { - foldJSBinaryOp( - if (op == ===) JSBinaryOp.=== else JSBinaryOp.!==, - lhs, rhs) - } else { - default + (lhs, rhs) match { + case (PreTransLit(l), PreTransLit(r)) => + val isSame = literal_===(l, r, isJSStrictEq = false) + PreTransLit(BooleanLiteral(if (op == ===) isSame else !isSame)) + case _ if canOptimizeAsJSStrictEq(lhs.tpe, rhs.tpe) => + foldJSBinaryOp( + if (op == ===) JSBinaryOp.=== else JSBinaryOp.!==, + lhs, rhs) + case _ => + default } case String_+ => - // Here things can be constant! val lhs1 = foldToStringForString_+(lhs) val rhs1 = foldToStringForString_+(rhs) @@ -3521,8 +3390,48 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { stringDefault } + case Boolean_== | Boolean_!= => + val positive = (op == Boolean_==) + (lhs, rhs) match { + case (PreTransLit(BooleanLiteral(l)), PreTransLit(BooleanLiteral(r))) => + booleanLit(if (positive) l == r else l != r) + case (PreTransLit(_), _) => + foldBinaryOp(op, rhs, lhs) + + case (PreTransLit(BooleanLiteral(l)), _) => + if (l == positive) rhs + else foldUnaryOp(UnaryOp.Boolean_!, rhs) + + case _ => + default + } + + case Boolean_| => + (lhs, rhs) match { + case (PreTransLit(BooleanLiteral(l)), PreTransLit(BooleanLiteral(r))) => + booleanLit(l | r) + + case (_, PreTransLit(BooleanLiteral(false))) => lhs + case (PreTransLit(BooleanLiteral(false)), _) => rhs + + case _ => default + } + + case Boolean_& => + (lhs, rhs) match { + case (PreTransLit(BooleanLiteral(l)), PreTransLit(BooleanLiteral(r))) => + booleanLit(l & r) + + case (_, PreTransLit(BooleanLiteral(true))) => lhs + case (PreTransLit(BooleanLiteral(true)), _) => rhs + + case _ => default + } + case Int_+ => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l + r) case (_, PreTransLit(IntLiteral(_))) => foldBinaryOp(Int_+, rhs, lhs) case (PreTransLit(IntLiteral(0)), _) => @@ -3538,6 +3447,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_- => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l - r) case (_, PreTransLit(IntLiteral(r))) => foldBinaryOp(Int_+, lhs, PreTransLit(IntLiteral(-r))) @@ -3558,6 +3469,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_* => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l * r) case (_, PreTransLit(IntLiteral(_))) => foldBinaryOp(Int_*, rhs, lhs) @@ -3585,6 +3498,11 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_/ => (lhs, rhs) match { + case (_, PreTransLit(IntLiteral(0))) => + default + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l / r) + case (_, PreTransLit(IntLiteral(1))) => lhs case (_, PreTransLit(IntLiteral(-1))) => @@ -3595,6 +3513,11 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_% => (lhs, rhs) match { + case (_, PreTransLit(IntLiteral(0))) => + default + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l % r) + case (_, PreTransLit(IntLiteral(1 | -1))) => Block(finishTransformStat(lhs), IntLiteral(0)).toPreTransform @@ -3603,6 +3526,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_| => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l | r) + case (_, PreTransLit(IntLiteral(_))) => foldBinaryOp(Int_|, rhs, lhs) case (PreTransLit(IntLiteral(0)), _) => rhs @@ -3618,6 +3544,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_& => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l & r) + case (_, PreTransLit(IntLiteral(_))) => foldBinaryOp(Int_&, rhs, lhs) case (PreTransLit(IntLiteral(-1)), _) => rhs @@ -3633,6 +3562,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_^ => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l ^ r) + case (_, PreTransLit(IntLiteral(_))) => foldBinaryOp(Int_^, rhs, lhs) case (PreTransLit(IntLiteral(0)), _) => rhs @@ -3645,6 +3577,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_<< => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l << r) + case (PreTransLit(IntLiteral(0)), _) => PreTransBlock(finishTransformStat(rhs), lhs) @@ -3668,6 +3603,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_>>> => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l >>> r) + case (PreTransLit(IntLiteral(0)), _) => PreTransBlock(finishTransformStat(rhs), lhs) @@ -3699,6 +3637,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Int_>> => (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + intLit(l >> r) + case (PreTransLit(IntLiteral(0 | -1)), _) => PreTransBlock(finishTransformStat(rhs), lhs) @@ -3721,8 +3662,85 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case _ => default } + case Int_== | Int_!= => + (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + booleanLit(if (op == Int_==) l == r else l != r) + + case (PreTransBinaryOp(Int_+, PreTransLit(IntLiteral(x)), y), + PreTransLit(IntLiteral(z))) => + foldBinaryOp(op, y, PreTransLit(IntLiteral(z - x))) + + case (PreTransBinaryOp(Int_-, PreTransLit(IntLiteral(x)), y), + PreTransLit(IntLiteral(z))) => + foldBinaryOp(op, y, PreTransLit(IntLiteral(x - z))) + + case (PreTransBinaryOp(Int_^, PreTransLit(IntLiteral(x)), y), + PreTransLit(IntLiteral(z))) => + foldBinaryOp(op, y, PreTransLit(IntLiteral(x ^ z))) + + case (PreTransLit(_), _) => foldBinaryOp(op, rhs, lhs) + + case _ => default + } + + case Int_< | Int_<= | Int_> | Int_>= => + def flippedOp = (op: @switch) match { + case Int_< => Int_> + case Int_<= => Int_>= + case Int_> => Int_< + case Int_>= => Int_<= + } + + (lhs, rhs) match { + case (PreTransLit(IntLiteral(l)), PreTransLit(IntLiteral(r))) => + booleanLit((op: @switch) match { + case Int_< => l < r + case Int_<= => l <= r + case Int_> => l > r + case Int_>= => l >= r + }) + + case (_, PreTransLit(IntLiteral(y))) => + y match { + case Int.MinValue => + if (op == Int_< || op == Int_>=) { + Block(finishTransformStat(lhs), + BooleanLiteral(op == Int_>=)).toPreTransform + } else { + foldBinaryOp(if (op == Int_<=) Int_== else Int_!=, lhs, rhs) + } + + case Int.MaxValue => + if (op == Int_> || op == Int_<=) { + Block(finishTransformStat(lhs), + BooleanLiteral(op == Int_<=)).toPreTransform + } else { + foldBinaryOp(if (op == Int_>=) Int_== else Int_!=, lhs, rhs) + } + + case _ if y == Int.MinValue + 1 && (op == Int_< || op == Int_>=) => + foldBinaryOp(if (op == Int_<) Int_== else Int_!=, lhs, + PreTransLit(IntLiteral(Int.MinValue))) + + case _ if y == Int.MaxValue - 1 && (op == Int_> || op == Int_<=) => + foldBinaryOp(if (op == Int_>) Int_== else Int_!=, lhs, + PreTransLit(IntLiteral(Int.MaxValue))) + + case _ => default + } + + case (PreTransLit(IntLiteral(_)), _) => + foldBinaryOp(flippedOp, rhs, lhs) + + case _ => default + } + case Long_+ => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l + r) + case (_, PreTransLit(LongLiteral(_))) => foldBinaryOp(Long_+, rhs, lhs) case (PreTransLit(LongLiteral(0)), _) => rhs @@ -3736,6 +3754,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_- => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l - r) + case (_, PreTransLit(LongLiteral(r))) => foldBinaryOp(Long_+, PreTransLit(LongLiteral(-r)), lhs) @@ -3755,6 +3776,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_* => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l * r) + case (_, PreTransLit(LongLiteral(_))) => foldBinaryOp(Long_*, rhs, lhs) @@ -3782,6 +3806,11 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_/ => (lhs, rhs) match { + case (_, PreTransLit(LongLiteral(0L))) => + default + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l / r) + case (_, PreTransLit(LongLiteral(1))) => lhs case (_, PreTransLit(LongLiteral(-1))) => @@ -3796,6 +3825,11 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_% => (lhs, rhs) match { + case (_, PreTransLit(LongLiteral(0L))) => + default + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l % r) + case (_, PreTransLit(LongLiteral(1L | -1L))) => Block(finishTransformStat(lhs), LongLiteral(0L)).toPreTransform @@ -3807,6 +3841,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_| => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l | r) + case (_, PreTransLit(LongLiteral(_))) => foldBinaryOp(Long_|, rhs, lhs) case (PreTransLit(LongLiteral(0)), _) => @@ -3824,6 +3861,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_& => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l & r) + case (_, PreTransLit(LongLiteral(_))) => foldBinaryOp(Long_&, rhs, lhs) case (PreTransLit(LongLiteral(-1)), _) => @@ -3841,6 +3881,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_^ => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + longLit(l ^ r) + case (_, PreTransLit(LongLiteral(_))) => foldBinaryOp(Long_^, rhs, lhs) case (PreTransLit(LongLiteral(0)), _) => @@ -3855,6 +3898,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_<< => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(IntLiteral(r))) => + longLit(l << r) + case (_, PreTransLit(IntLiteral(x))) if x % 64 == 0 => lhs case _ => default @@ -3862,6 +3908,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_>>> => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(IntLiteral(r))) => + longLit(l >>> r) + case (_, PreTransLit(IntLiteral(x))) if x % 64 == 0 => lhs case _ => default @@ -3869,6 +3918,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_>> => (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(IntLiteral(r))) => + longLit(l >> r) + case (_, PreTransLit(IntLiteral(x))) if x % 64 == 0 => lhs case _ => default @@ -3877,6 +3929,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Long_== | Long_!= => val positive = (op == Long_==) (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + booleanLit(if (op == Long_==) l == r else l != r) + case (LongFromInt(x), LongFromInt(y)) => foldBinaryOp(if (positive) Int_== else Int_!=, x, y) case (LongFromInt(x), PreTransLit(LongLiteral(y))) => @@ -3916,6 +3971,14 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } (lhs, rhs) match { + case (PreTransLit(LongLiteral(l)), PreTransLit(LongLiteral(r))) => + booleanLit((op: @switch) match { + case Long_< => l < r + case Long_<= => l <= r + case Long_> => l > r + case Long_>= => l >= r + }) + case (_, PreTransLit(LongLiteral(Long.MinValue))) => if (op == Long_< || op == Long_>=) { Block(finishTransformStat(lhs), @@ -4018,8 +4081,27 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case _ => default } + case Float_+ => + (lhs, rhs) match { + case (PreTransLit(FloatLiteral(l)), PreTransLit(FloatLiteral(r))) => + floatLit(l + r) + + case _ => default + } + + case Float_- => + (lhs, rhs) match { + case (PreTransLit(FloatLiteral(l)), PreTransLit(FloatLiteral(r))) => + floatLit(l - r) + + case _ => default + } + case Float_* => (lhs, rhs) match { + case (PreTransLit(FloatLiteral(l)), PreTransLit(FloatLiteral(r))) => + floatLit(l * r) + case (_, PreTransLit(FloatLiteral(_))) => foldBinaryOp(Float_*, rhs, lhs) @@ -4034,6 +4116,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Float_/ => (lhs, rhs) match { + case (PreTransLit(FloatLiteral(l)), PreTransLit(FloatLiteral(r))) => + floatLit(l / r) + case (_, PreTransLit(FloatLiteral(1))) => lhs case (_, PreTransLit(FloatLiteral(-1))) => @@ -4044,11 +4129,33 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Float_% => (lhs, rhs) match { + case (PreTransLit(FloatLiteral(l)), PreTransLit(FloatLiteral(r))) => + floatLit(l % r) + + case _ => default + } + + case Double_+ => + (lhs, rhs) match { + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + doubleLit(l + r) + + case _ => default + } + + case Double_- => + (lhs, rhs) match { + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + doubleLit(l - r) + case _ => default } case Double_* => (lhs, rhs) match { + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + doubleLit(l * r) + case (_, PreTransLit(DoubleLiteral(_))) => foldBinaryOp(Double_*, rhs, lhs) @@ -4063,6 +4170,9 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Double_/ => (lhs, rhs) match { + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + doubleLit(l / r) + case (_, PreTransLit(DoubleLiteral(1))) => lhs case (_, PreTransLit(DoubleLiteral(-1))) => @@ -4073,104 +4183,70 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { case Double_% => (lhs, rhs) match { + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + doubleLit(l % r) + case _ => default } - case Boolean_== | Boolean_!= => - val positive = (op == Boolean_==) + case Double_== => (lhs, rhs) match { - case (PreTransLit(_), _) => - foldBinaryOp(op, rhs, lhs) - - case (PreTransLit(BooleanLiteral(l)), _) => - if (l == positive) rhs - else foldUnaryOp(UnaryOp.Boolean_!, rhs) + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + booleanLit(l == r) - case _ => - default + case _ => default } - case Boolean_| => + case Double_!= => (lhs, rhs) match { - case (_, PreTransLit(BooleanLiteral(false))) => lhs - case (PreTransLit(BooleanLiteral(false)), _) => rhs + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + booleanLit(l != r) case _ => default } - case Boolean_& => + case Double_< => (lhs, rhs) match { - case (_, PreTransLit(BooleanLiteral(true))) => lhs - case (PreTransLit(BooleanLiteral(true)), _) => rhs + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + booleanLit(l < r) case _ => default } - case Int_== | Int_!= => + case Double_<= => (lhs, rhs) match { - case (PreTransBinaryOp(Int_+, PreTransLit(IntLiteral(x)), y), - PreTransLit(IntLiteral(z))) => - foldBinaryOp(op, y, PreTransLit(IntLiteral(z - x))) - - case (PreTransBinaryOp(Int_-, PreTransLit(IntLiteral(x)), y), - PreTransLit(IntLiteral(z))) => - foldBinaryOp(op, y, PreTransLit(IntLiteral(x - z))) - - case (PreTransBinaryOp(Int_^, PreTransLit(IntLiteral(x)), y), - PreTransLit(IntLiteral(z))) => - foldBinaryOp(op, y, PreTransLit(IntLiteral(x ^ z))) - - case (PreTransLit(_), _) => foldBinaryOp(op, rhs, lhs) + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + booleanLit(l <= r) case _ => default } - case Int_< | Int_<= | Int_> | Int_>= => - def flippedOp = (op: @switch) match { - case Int_< => Int_> - case Int_<= => Int_>= - case Int_> => Int_< - case Int_>= => Int_<= - } - + case Double_> => (lhs, rhs) match { - case (_, PreTransLit(IntLiteral(y))) => - y match { - case Int.MinValue => - if (op == Int_< || op == Int_>=) { - Block(finishTransformStat(lhs), - BooleanLiteral(op == Int_>=)).toPreTransform - } else { - foldBinaryOp(if (op == Int_<=) Int_== else Int_!=, lhs, rhs) - } + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + booleanLit(l > r) - case Int.MaxValue => - if (op == Int_> || op == Int_<=) { - Block(finishTransformStat(lhs), - BooleanLiteral(op == Int_<=)).toPreTransform - } else { - foldBinaryOp(if (op == Int_>=) Int_== else Int_!=, lhs, rhs) - } - - case _ if y == Int.MinValue + 1 && (op == Int_< || op == Int_>=) => - foldBinaryOp(if (op == Int_<) Int_== else Int_!=, lhs, - PreTransLit(IntLiteral(Int.MinValue))) + case _ => default + } - case _ if y == Int.MaxValue - 1 && (op == Int_> || op == Int_<=) => - foldBinaryOp(if (op == Int_>) Int_== else Int_!=, lhs, - PreTransLit(IntLiteral(Int.MaxValue))) + case Double_>= => + (lhs, rhs) match { + case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) => + booleanLit(l >= r) - case _ => default - } + case _ => default + } - case (PreTransLit(IntLiteral(_)), _) => - foldBinaryOp(flippedOp, rhs, lhs) + case String_charAt => + (lhs, rhs) match { + case (PreTransLit(StringLiteral(l)), PreTransLit(IntLiteral(r))) => + if (r < 0 || r >= l.length()) + default + else + charLit(l.charAt(r)) case _ => default } - - case _ => - default } } From cb80afb9db6fa530c7c56d8f9e39cae15cb00718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Aug 2022 19:13:17 +0200 Subject: [PATCH 48/75] Embed enough information in the IR for NPE semantics of `throw null`. In Scala, as in Java, `throw (null: Throwable)` throws a `NullPointerException`. Since NPEs are Undefined Behaviors in Scala.js, we previously did not care about this pattern, and assume it would not happen in the codegen. If we want to have a checked behavior for NPEs, we need enough information in the IR for the linker to be able to conditionally throw accurate `NullPointerException`s in that case. We do this with a combination of: * Make `UnwrapFromThrowable(null)` "throw" an NPE, subject to undefined behavior (and potentially a checked behavior in the future), and * Emit `Throw(UnwrapFromThrowable(e))` when `e` is nullable, even if it cannot be a `JavaScriptException`. We fix up the non-null-aware IR from previous versions of the compiler via a deserialization hack. The produced JavaScript code is basically unchanged after this commit, since the optimizer can remove the `UnwrapFromThrowable` in all the cases where the compiler previously omitted them. Some local variables have different names, and outer pointer checks in inner JS classes have a worse error path, but that is negligible. --- .../org/scalajs/nscplugin/GenJSCode.scala | 12 +-- .../scala/org/scalajs/ir/Serializers.scala | 87 ++++++++++++++++++- .../scala/scalajs/js/special/package.scala | 3 + .../frontend/optimizer/OptimizerCore.scala | 11 ++- .../testsuite/jsinterop/SpecialTest.scala | 3 +- 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 3e16028af5..c4bab865a7 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2429,11 +2429,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case Throw(expr) => val ex = genExpr(expr) - js.Throw { - if (!ex.isInstanceOf[js.Null] && isMaybeJavaScriptException(expr.tpe)) - js.UnwrapFromThrowable(ex) - else - ex + ex match { + case js.New(cls, _, _) if cls != JavaScriptExceptionClassName => + // Common case where ex is neither null nor a js.JavaScriptException + js.Throw(ex) + case _ => + js.Throw(js.UnwrapFromThrowable(ex)) } /* !!! Copy-pasted from `CleanUp.scala` upstream and simplified with @@ -7065,6 +7066,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private object GenJSCode { private val JSObjectClassName = ClassName("scala.scalajs.js.Object") + private val JavaScriptExceptionClassName = ClassName("scala.scalajs.js.JavaScriptException") private val newSimpleMethodName = SimpleMethodName("new") 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 219bacb632..01897cf691 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -1119,8 +1119,14 @@ object Serializers { case TagTryFinally => TryFinally(readTree(), readTree()) - case TagThrow => Throw(readTree()) - case TagMatch => + 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()) @@ -1230,6 +1236,83 @@ object Serializers { } } + /** Patches the argument of a `Throw` for IR version until 1.8. + * + * 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 + * subject to UB so not really) when `e` is a `null` `Throwable`. + * + * Moreover, there was no other user-space way to emit a `Throw(e)` in the + * IR (`js.special.throw` was introduced in 1.11), so *all* `Throw` nodes + * are part of the semantics of a Scala `throw expr` or of an implicit + * re-throw in a Scala `try..catch`. + * + * In Scala.js 1.11, we explicitly ruled out the NPE behavior of `Throw`, + * 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`. + * + * With this hack, we patch `Throw(e)` where `e` is a nullable `Throwable` + * by inserting an appropriate `UnwrapFromThrowable`. + * + * 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: + * + * {{{ + * if (expr === null) unwrapFromThrowable(null) else expr + * }}} + * + * 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. + * + * 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 + * + * - 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)`. + */ + 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) + 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)) + } + + case _ => + // Do not transform expressions of other types, in particular `AnyType` + expr + } + } + def readTrees(): List[Tree] = List.fill(readInt())(readTree()) diff --git a/library/src/main/scala/scala/scalajs/js/special/package.scala b/library/src/main/scala/scala/scalajs/js/special/package.scala index 1c2437bff9..2ad1da9ece 100644 --- a/library/src/main/scala/scala/scalajs/js/special/package.scala +++ b/library/src/main/scala/scala/scalajs/js/special/package.scala @@ -175,6 +175,9 @@ package object special { * * Instances of [[js.JavaScriptException]] are unwrapped to return the * underlying value. Other values are returned as is. + * + * @throws java.lang.NullPointerException + * If `th` is `null`. Subject to Undefined Behaviors. */ def unwrapFromThrowable(th: Throwable): scala.Any = throw new java.lang.Error("stub") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 5d8207bccb..5df0d854c4 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -978,7 +978,14 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { def default = cont(PreTransTree(UnwrapFromThrowable(finishTransformExpr(texpr)))) - if (isSubtype(texpr.tpe.base, JavaScriptExceptionClassType)) { + val baseTpe = texpr.tpe.base + + if (baseTpe == NothingType) { + cont(texpr) + } else if (baseTpe == NullType) { + // Undefined behavior for NPE + cont(texpr) + } else if (isSubtype(baseTpe, JavaScriptExceptionClassType)) { if (texpr.tpe.isNullable) { default } else { @@ -986,7 +993,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { FieldIdent(exceptionFieldName), isLhsOfAssign = false)(cont) } } else { - if (texpr.tpe.isExact || !isSubtype(JavaScriptExceptionClassType, texpr.tpe.base)) + if (texpr.tpe.isExact || !isSubtype(JavaScriptExceptionClassType, baseTpe)) cont(texpr) else default diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala index af26ab603b..e96e4df314 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala @@ -225,8 +225,7 @@ class SpecialTest { val th = new IllegalArgumentException assertSame(th, js.special.unwrapFromThrowable(th)) - // Does not unwrap null - assertNull(null, js.special.unwrapFromThrowable(null)) + // unwrapFromThrowable(null) is UB (as NullPointerException) and is therefore not tested } // js.special.fileLevelThis From d50b69fc52f8559aea33f9c4dd2de74eac91ea74 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 5 Jun 2022 12:14:09 +0200 Subject: [PATCH 49/75] Fix #2227: Remove unused fields --- .../scalajs/linker/analyzer/Analysis.scala | 8 +- .../scalajs/linker/analyzer/Analyzer.scala | 82 +++++--- .../org/scalajs/linker/analyzer/Infos.scala | 87 +++++--- .../scalajs/linker/frontend/BaseLinker.scala | 11 +- .../org/scalajs/linker/frontend/Refiner.scala | 11 +- .../frontend/optimizer/IncOptimizer.scala | 51 ++++- .../frontend/optimizer/OptimizerCore.scala | 185 +++++++++++------- .../scalajs/linker/standard/LinkedClass.scala | 12 +- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- .../org/scalajs/linker/OptimizerTest.scala | 81 ++++++++ project/BinaryIncompatibilities.scala | 6 +- project/Build.scala | 24 +-- .../testsuite/compiler/OptimizerTest.scala | 60 ++++++ 13 files changed, 475 insertions(+), 149 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala index 13ab61a9fa..43b3850d8c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala @@ -64,8 +64,12 @@ object Analysis { def isModuleAccessed: Boolean def areInstanceTestsUsed: Boolean def isDataAccessed: Boolean - def isAnyStaticFieldUsed: Boolean - def isAnyPrivateJSFieldUsed: Boolean + + def fieldsRead: scala.collection.Set[FieldName] + def fieldsWritten: scala.collection.Set[FieldName] + def staticFieldsRead: scala.collection.Set[FieldName] + def staticFieldsWritten: scala.collection.Set[FieldName] + def jsNativeMembersUsed: scala.collection.Set[MethodName] def staticDependencies: scala.collection.Set[ClassName] diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala index a91957ff4e..5e2886bebc 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala @@ -574,8 +574,11 @@ private final class Analyzer(config: CommonPhaseConfig, var isModuleAccessed: Boolean = false var areInstanceTestsUsed: Boolean = false var isDataAccessed: Boolean = false - var isAnyStaticFieldUsed: Boolean = false - var isAnyPrivateJSFieldUsed: Boolean = false + + val fieldsRead: mutable.Set[FieldName] = mutable.Set.empty + val fieldsWritten: mutable.Set[FieldName] = mutable.Set.empty + val staticFieldsRead: mutable.Set[FieldName] = mutable.Set.empty + val staticFieldsWritten: mutable.Set[FieldName] = mutable.Set.empty val jsNativeMembersUsed: mutable.Set[MethodName] = mutable.Set.empty @@ -942,18 +945,8 @@ private final class Analyzer(config: CommonPhaseConfig, (isScalaClass || isJSClass || isNativeJSClass)) { isInstantiated = true - /* Reach referenced classes of non-static fields - * - * Note that the classes referenced by static fields are reached - * implicitly by the call-sites that read or write the field: the - * SelectStatic expression has the same type as the field. - * - * We do not need to add this to staticDependencies: The definition - * site will not reference the classes in the final JS code. - */ // TODO: Why is this not in subclassInstantiated()? - for (className <- data.referencedFieldClasses) - lookupClass(className)(_ => ()) + referenceFieldClasses(fieldsRead ++ fieldsWritten) if (isScalaClass) { accessData() @@ -1082,6 +1075,18 @@ private final class Analyzer(config: CommonPhaseConfig, lookupMethod(methodName).reachStatic() } + def readFields(names: List[FieldName])(implicit from: From): Unit = { + fieldsRead ++= names + if (isInstantiated) + referenceFieldClasses(names) + } + + def writeFields(names: List[FieldName])(implicit from: From): Unit = { + fieldsWritten ++= names + if (isInstantiated) + referenceFieldClasses(names) + } + def useJSNativeMember(name: MethodName)( implicit from: From): Option[JSNativeLoadSpec] = { val maybeJSNativeLoadSpec = data.jsNativeMembers.get(name) @@ -1096,6 +1101,23 @@ private final class Analyzer(config: CommonPhaseConfig, maybeJSNativeLoadSpec } + private def referenceFieldClasses(fieldNames: Iterable[FieldName])( + implicit from: From): Unit = { + assert(isInstantiated) + + /* Reach referenced classes of non-static fields + * + * We do not need to add this to staticDependencies: The definition + * site will not reference the classes in the final JS code. + */ + for { + fieldName <- fieldNames + className <- data.referencedFieldClasses.get(fieldName) + } { + lookupClass(className)(_ => ()) + } + } + private def validateLoadSpec(jsNativeLoadSpec: JSNativeLoadSpec, jsNativeMember: Option[MethodName])(implicit from: From): Unit = { if (isNoModule) { @@ -1269,35 +1291,39 @@ private final class Analyzer(config: CommonPhaseConfig, lookupClass(className)(_ => ()) } + for (className <- data.staticallyReferencedClasses) { + staticDependencies += className + lookupClass(className)(_ => ()) + } + /* `for` loops on maps are written with `while` loops to help the JIT * compiler to inline and stack allocate tuples created by the iterators */ - val privateJSFieldsUsedIterator = data.privateJSFieldsUsed.iterator - while (privateJSFieldsUsedIterator.hasNext) { - val (className, fields) = privateJSFieldsUsedIterator.next() - if (fields.nonEmpty) { - staticDependencies += className - lookupClass(className)(_.isAnyPrivateJSFieldUsed = true) - } + val fieldsReadIterator = data.fieldsRead.iterator + while (fieldsReadIterator.hasNext) { + val (className, fields) = fieldsReadIterator.next() + lookupClass(className)(_.readFields(fields)) + } + + val fieldsWrittenIterator = data.fieldsWritten.iterator + while (fieldsWrittenIterator.hasNext) { + val (className, fields) = fieldsWrittenIterator.next() + lookupClass(className)(_.writeFields(fields)) } val staticFieldsReadIterator = data.staticFieldsRead.iterator while (staticFieldsReadIterator.hasNext) { val (className, fields) = staticFieldsReadIterator.next() - if (fields.nonEmpty) { - staticDependencies += className - lookupClass(className)(_.isAnyStaticFieldUsed = true) - } + staticDependencies += className + lookupClass(className)(_.staticFieldsRead ++= fields) } val staticFieldsWrittenIterator = data.staticFieldsWritten.iterator while (staticFieldsWrittenIterator.hasNext) { val (className, fields) = staticFieldsWrittenIterator.next() - if (fields.nonEmpty) { - staticDependencies += className - lookupClass(className)(_.isAnyStaticFieldUsed = true) - } + staticDependencies += className + lookupClass(className)(_.staticFieldsWritten ++= fields) } val methodsCalledIterator = data.methodsCalled.iterator diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala index 6bc7eb807f..347142643b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala @@ -49,7 +49,16 @@ object Infos { val superClass: Option[ClassName], // always None for interfaces val interfaces: List[ClassName], // direct parent interfaces only val jsNativeLoadSpec: Option[JSNativeLoadSpec], - val referencedFieldClasses: List[ClassName], + /* Referenced classes of non-static fields. + * + * Note that the classes referenced by static fields are reached + * implicitly by the call-sites that read or write the field: the + * SelectStatic expression has the same type as the field. + * + * Further, note that some existing fields might not appear in this map: + * This happens when they have non-class type (e.g. Int) + */ + val referencedFieldClasses: Map[FieldName, ClassName], val methods: List[MethodInfo], val jsNativeMembers: Map[MethodName, JSNativeLoadSpec], val exportedMembers: List[ReachabilityInfo] @@ -84,7 +93,8 @@ object Infos { ) final class ReachabilityInfo private[Infos] ( - val privateJSFieldsUsed: Map[ClassName, List[FieldName]], + val fieldsRead: Map[ClassName, List[FieldName]], + val fieldsWritten: Map[ClassName, List[FieldName]], val staticFieldsRead: Map[ClassName, List[FieldName]], val staticFieldsWritten: Map[ClassName, List[FieldName]], val methodsCalled: Map[ClassName, List[MethodName]], @@ -99,6 +109,7 @@ object Infos { val usedInstanceTests: List[ClassName], val accessedClassData: List[ClassName], val referencedClasses: List[ClassName], + val staticallyReferencedClasses: List[ClassName], val accessedClassClass: Boolean, val accessedNewTarget: Boolean, val accessedImportMeta: Boolean @@ -107,8 +118,8 @@ object Infos { object ReachabilityInfo { val Empty: ReachabilityInfo = { new ReachabilityInfo(Map.empty, Map.empty, Map.empty, Map.empty, - Map.empty, Map.empty, Map.empty, Nil, Nil, Nil, Nil, Nil, false, - false, false) + Map.empty, Map.empty, Map.empty, Map.empty, Nil, Nil, Nil, Nil, Nil, Nil, + false, false, false) } } @@ -119,17 +130,17 @@ object Infos { private val interfaces: List[ClassName], private val jsNativeLoadSpec: Option[JSNativeLoadSpec] ) { - private val referencedFieldClasses = mutable.Set.empty[ClassName] + private val referencedFieldClasses = mutable.Map.empty[FieldName, ClassName] private val methods = mutable.ListBuffer.empty[MethodInfo] private val jsNativeMembers = mutable.Map.empty[MethodName, JSNativeLoadSpec] private val exportedMembers = mutable.ListBuffer.empty[ReachabilityInfo] - def maybeAddReferencedFieldClass(tpe: Type): this.type = { + def maybeAddReferencedFieldClass(name: FieldName, tpe: Type): this.type = { tpe match { case ClassType(cls) => - referencedFieldClasses += cls + referencedFieldClasses.put(name, cls) case ArrayType(ArrayTypeRef(ClassRef(cls), _)) => - referencedFieldClasses += cls + referencedFieldClasses.put(name, cls) case _ => } @@ -153,13 +164,14 @@ object Infos { def result(): ClassInfo = { new ClassInfo(className, kind, superClass, - interfaces, jsNativeLoadSpec, referencedFieldClasses.toList, + interfaces, jsNativeLoadSpec, referencedFieldClasses.toMap, methods.toList, jsNativeMembers.toMap, exportedMembers.toList) } } final class ReachabilityInfoBuilder { - private val privateJSFieldsUsed = mutable.Map.empty[ClassName, mutable.Set[FieldName]] + private val fieldsRead = mutable.Map.empty[ClassName, mutable.Set[FieldName]] + private val fieldsWritten = mutable.Map.empty[ClassName, mutable.Set[FieldName]] private val staticFieldsRead = mutable.Map.empty[ClassName, mutable.Set[FieldName]] private val staticFieldsWritten = mutable.Map.empty[ClassName, mutable.Set[FieldName]] private val methodsCalled = mutable.Map.empty[ClassName, mutable.Set[MethodName]] @@ -171,12 +183,18 @@ object Infos { private val usedInstanceTests = mutable.Set.empty[ClassName] private val accessedClassData = mutable.Set.empty[ClassName] private val referencedClasses = mutable.Set.empty[ClassName] + private val staticallyReferencedClasses = mutable.Set.empty[ClassName] private var accessedClassClass = false private var accessedNewTarget = false private var accessedImportMeta = false - def addPrivateJSFieldUsed(cls: ClassName, field: FieldName): this.type = { - privateJSFieldsUsed.getOrElseUpdate(cls, mutable.Set.empty) += field + def addFieldRead(cls: ClassName, field: FieldName): this.type = { + fieldsRead.getOrElseUpdate(cls, mutable.Set.empty) += field + this + } + + def addFieldWritten(cls: ClassName, field: FieldName): this.type = { + fieldsWritten.getOrElseUpdate(cls, mutable.Set.empty) += field this } @@ -311,6 +329,11 @@ object Infos { this } + def addStaticallyReferencedClass(cls: ClassName): this.type = { + staticallyReferencedClasses += cls + this + } + def maybeAddReferencedClass(tpe: Type): this.type = { tpe match { case ClassType(cls) => @@ -342,7 +365,8 @@ object Infos { m.map(kv => kv._1 -> kv._2.toList).toMap new ReachabilityInfo( - privateJSFieldsUsed = toMapOfLists(privateJSFieldsUsed), + fieldsRead = toMapOfLists(fieldsRead), + fieldsWritten = toMapOfLists(fieldsWritten), staticFieldsRead = toMapOfLists(staticFieldsRead), staticFieldsWritten = toMapOfLists(staticFieldsWritten), methodsCalled = toMapOfLists(methodsCalled), @@ -354,6 +378,7 @@ object Infos { usedInstanceTests = usedInstanceTests.toList, accessedClassData = accessedClassData.toList, referencedClasses = referencedClasses.toList, + staticallyReferencedClasses = staticallyReferencedClasses.toList, accessedClassClass = accessedClassClass, accessedNewTarget = accessedNewTarget, accessedImportMeta = accessedImportMeta @@ -370,8 +395,13 @@ object Infos { classDef.jsNativeLoadSpec) classDef.memberDefs foreach { - case fieldDef: AnyFieldDef => - builder.maybeAddReferencedFieldClass(fieldDef.ftpe) + case FieldDef(flags, FieldIdent(name), _, ftpe) => + if (!flags.namespace.isStatic) { + builder.maybeAddReferencedFieldClass(name, ftpe) + } + + case _: JSFieldDef => + // Nothing to do. case methodDef: MethodDef => builder.addMethod(generateMethodInfo(methodDef)) @@ -510,11 +540,23 @@ object Infos { builder.maybeAddReferencedClass(tree.tpe) tree match { - /* Do not call super.traverse() so that the field is not also marked as + /* Do not call super.traverse() so that fields are not also marked as * read. */ - case Assign(SelectStatic(className, field), rhs) => - builder.addStaticFieldWritten(className, field.name) + case Assign(lhs, rhs) => + lhs match { + case Select(qualifier, className, field) => + builder.addFieldWritten(className, field.name) + traverse(qualifier) + case SelectStatic(className, field) => + builder.addStaticFieldWritten(className, field.name) + case JSPrivateSelect(qualifier, className, field) => + builder.addStaticallyReferencedClass(className) // for the private name of the field + builder.addFieldWritten(className, field.name) + traverse(qualifier) + case _ => + traverse(lhs) + } traverse(rhs) // In all other cases, we'll have to call super.traverse() @@ -523,8 +565,8 @@ object Infos { case New(className, ctor, _) => builder.addInstantiatedClass(className, ctor.name) - case Select(_, className, _) => - builder.addReferencedClass(className) + case Select(_, className, field) => + builder.addFieldRead(className, field.name) case SelectStatic(className, field) => builder.addStaticFieldRead(className, field.name) case SelectJSNativeMember(className, member) => @@ -614,8 +656,9 @@ object Infos { case UnwrapFromThrowable(_) => builder.addUsedInstanceTest(JavaScriptExceptionClass) - case JSPrivateSelect(qualifier, className, field) => - builder.addPrivateJSFieldUsed(className, field.name) + case JSPrivateSelect(_, className, field) => + builder.addStaticallyReferencedClass(className) // for the private name of the field + builder.addFieldRead(className, field.name) case JSNewTarget() => builder.addAccessNewTarget() diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala index 4757dd6080..4d260cb18e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala @@ -224,6 +224,8 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { hasInstances = analyzerInfo.isAnySubclassInstantiated, hasInstanceTests = analyzerInfo.areInstanceTestsUsed, hasRuntimeTypeInfo = analyzerInfo.isDataAccessed, + fieldsRead = analyzerInfo.fieldsRead.toSet, + staticFieldsRead = analyzerInfo.staticFieldsRead.toSet, staticDependencies = analyzerInfo.staticDependencies.toSet, externalDependencies = analyzerInfo.externalDependencies.toSet, dynamicDependencies = analyzerInfo.dynamicDependencies.toSet, @@ -238,9 +240,12 @@ private[frontend] object BaseLinker { field match { case field: FieldDef => - if (field.flags.namespace.isStatic) classInfo.isAnyStaticFieldUsed - else if (classInfo.kind.isJSType) classInfo.isAnyPrivateJSFieldUsed - else classInfo.isAnySubclassInstantiated + if (field.flags.namespace.isStatic) + classInfo.staticFieldsRead(field.name.name) || classInfo.staticFieldsWritten(field.name.name) + else if (classInfo.kind.isJSClass || classInfo.isAnySubclassInstantiated) + classInfo.fieldsRead(field.name.name) || classInfo.fieldsWritten(field.name.name) + else + false case field: JSFieldDef => classInfo.isAnySubclassInstantiated diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala index 7c87e9ecd1..31d318d395 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala @@ -118,6 +118,8 @@ final class Refiner(config: CommonPhaseConfig) { hasInstances = info.isAnySubclassInstantiated, hasInstanceTests = info.areInstanceTestsUsed, hasRuntimeTypeInfo = info.isDataAccessed, + fieldsRead = info.fieldsRead.toSet, + staticFieldsRead = info.staticFieldsRead.toSet, staticDependencies = info.staticDependencies.toSet, externalDependencies = info.externalDependencies.toSet, dynamicDependencies = info.dynamicDependencies.toSet @@ -193,8 +195,13 @@ private object Refiner { linkedClass.kind, linkedClass.superClass.map(_.name), linkedClass.interfaces.map(_.name), linkedClass.jsNativeLoadSpec) - for (field <- linkedClass.fields) - builder.maybeAddReferencedFieldClass(field.ftpe) + for { + FieldDef(flags, FieldIdent(name), _, ftpe) <- linkedClass.fields + if !flags.namespace.isStatic + } { + builder.maybeAddReferencedFieldClass(name, ftpe) + } + for (linkedMethod <- linkedClass.methods) builder.addMethod(methodsInfoCaches.getInfo(linkedMethod)) for (jsNativeMember <- linkedClass.jsNativeMembers) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 16de081a94..f4345a3b78 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -354,6 +354,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: private val hasElidableModuleAccessorAskers = collOps.emptyMap[MethodImpl, Unit] var fields: List[AnyFieldDef] = linkedClass.fields + var fieldsRead: Set[FieldName] = linkedClass.fieldsRead var tryNewInlineable: Option[OptimizerCore.InlineableClassStructure] = None setupAfterCreation(linkedClass) @@ -426,6 +427,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: updateWith(linkedClass) fields = linkedClass.fields + fieldsRead = linkedClass.fieldsRead val oldInterfaces = interfaces val newInterfaces = linkedClass.ancestors.map(getInterface).toSet @@ -541,18 +543,16 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } else { val allFields = for { parent <- reverseParentChain - field <- parent.fields - if !field.flags.namespace.isStatic + anyField <- parent.fields + if !anyField.flags.namespace.isStatic + // non-JS class may only contain FieldDefs (no JSFieldDef) + field = anyField.asInstanceOf[FieldDef] + if parent.fieldsRead.contains(field.name.name) } yield { parent.className -> field } - if (allFields.forall(_._2.isInstanceOf[FieldDef])) { - Some(new OptimizerCore.InlineableClassStructure( - allFields.asInstanceOf[List[(ClassName, FieldDef)]])) - } else { - None - } + Some(new OptimizerCore.InlineableClassStructure(allFields)) } tryNewInlineable != oldTryNewInlineable @@ -712,6 +712,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: mutable.ArrayBuffer.fill[MethodCallers](MemberNamespace.Count)(collOps.emptyMap) private val jsNativeImportsAskers = collOps.emptyMap[MethodImpl, Unit] + private val fieldsReadAskers = collOps.emptyMap[MethodImpl, Unit] private var _ancestors: List[ClassName] = linkedClass.ancestors @@ -734,6 +735,13 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: private var jsNativeImports: JSNativeImports = computeJSNativeImports(linkedClass) + /* Similar comment than for JS native imports: + * We track read state of all fields together to avoid too much tracking. + */ + + private var fieldsRead: Set[FieldName] = linkedClass.fieldsRead + private var staticFieldsRead: Set[FieldName] = linkedClass.staticFieldsRead + override def toString(): String = s"intf ${className.nameString}" @@ -797,6 +805,18 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: jsNativeImports._2.get(methodName) } + def askFieldRead(name: FieldName, asker: MethodImpl): Boolean = { + fieldsReadAskers.put(asker, ()) + asker.registerTo(this) + fieldsRead.contains(name) + } + + def askStaticFieldRead(name: FieldName, asker: MethodImpl): Boolean = { + fieldsReadAskers.put(asker, ()) + asker.registerTo(this) + staticFieldsRead.contains(name) + } + @inline def staticLike(namespace: MemberNamespace): StaticLikeNamespace = staticLikes(namespace.ordinal) @@ -818,6 +838,15 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: jsNativeImportsAskers.clear() } + // Update fields read. + if (fieldsRead != linkedClass.fieldsRead || + staticFieldsRead != linkedClass.staticFieldsRead) { + fieldsRead = linkedClass.fieldsRead + staticFieldsRead = linkedClass.staticFieldsRead + fieldsReadAskers.keysIterator.foreach(_.tag()) + fieldsReadAskers.clear() + } + // Update static likes for (staticLike <- staticLikes) { val (_, changed, _) = staticLike.updateWith(linkedClass) @@ -1064,6 +1093,12 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: getInterface(className).askJSNativeImport(methodName, myself) } } + + protected def isFieldRead(className: ClassName, fieldName: FieldName): Boolean = + getInterface(className).askFieldRead(fieldName, myself) + + protected def isStaticFieldRead(className: ClassName, fieldName: FieldName): Boolean = + getInterface(className).askStaticFieldRead(fieldName, myself) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 5d8207bccb..509863efbd 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -88,6 +88,12 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { protected def getJSNativeImportOf( target: ImportTarget): Option[JSNativeLoadSpec.Import] + /** Returns true if the given (non-static) field is ever read. */ + protected def isFieldRead(className: ClassName, fieldName: FieldName): Boolean + + /** Returns true if the given static field is ever read. */ + protected def isStaticFieldRead(className: ClassName, fieldName: FieldName): Boolean + private val localNameAllocator = new FreshNameAllocator.Local /** An allocated local variable name is mutable iff it belongs to this set. */ @@ -331,53 +337,39 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } case Assign(lhs, rhs) => - val cont = { (preTransLhs: PreTransform) => - resolveLocalDef(preTransLhs) match { - case PreTransRecordTree(lhsTree, lhsOrigType, lhsCancelFun) => - val recordType = lhsTree.tpe.asInstanceOf[RecordType] - - def buildInner(trhs: PreTransform): TailRec[Tree] = { - resolveLocalDef(trhs) match { - case PreTransRecordTree(rhsTree, rhsOrigType, rhsCancelFun) => - if (rhsTree.tpe != recordType || rhsOrigType != lhsOrigType) - lhsCancelFun() - TailCalls.done(Assign(lhsTree.asInstanceOf[AssignLhs], rhsTree)) - case _ => - lhsCancelFun() - } - } - - pretransformExpr(rhs) { trhs => - (trhs.tpe.base, lhsOrigType) match { - case (LongType, RefinedType( - ClassType(LongImpl.RuntimeLongClass), true, false)) => - /* The lhs is a stack-allocated RuntimeLong, but the rhs is - * a primitive Long. We expand the primitive Long into a - * new stack-allocated RuntimeLong so that we do not need - * to cancel. - */ - expandLongValue(trhs) { expandedRhs => - buildInner(expandedRhs) - } - - case _ => - buildInner(trhs) - } - } - - case PreTransTree(lhsTree, _) => - TailCalls.done(Assign(lhsTree.asInstanceOf[AssignLhs], transformExpr(rhs))) + val cont = { (tlhs: PreTransform) => + pretransformExpr(rhs) { trhs => + pretransformAssign(tlhs, trhs)(finishTransform(isStat)) } } - trampoline { - lhs match { - case lhs: Select => + + lhs match { + case Select(qualifier, className, FieldIdent(name)) if !isFieldRead(className, name) => + // Field is never read. Drop assign, keep side effects only. + Block(transformStat(qualifier), transformStat(rhs)) + + case SelectStatic(className, FieldIdent(name)) if !isStaticFieldRead(className, name) => + // Field is never read. Drop assign, keep side effects only. + transformStat(rhs) + + case JSPrivateSelect(qualifier, className, FieldIdent(name)) if !isFieldRead(className, name) => + // Field is never read. Drop assign, keep side effects only. + Block(transformStat(qualifier), transformStat(rhs)) + + case lhs: Select => + trampoline { pretransformSelectCommon(lhs, isLhsOfAssign = true)(cont) - case lhs: JSSelect => + } + + case lhs: JSSelect => + trampoline { pretransformJSSelect(lhs, isLhsOfAssign = true)(cont) - case _ => + } + + case _ => + trampoline { pretransformExpr(lhs)(cont) - } + } } case Return(expr, label) => @@ -1204,6 +1196,10 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { isLhsOfAssign: Boolean)( cont: PreTransCont)( implicit scope: Scope, pos: Position): TailRec[Tree] = { + /* Note: Callers are expected to have already removed writes to fields that + * are never read. + */ + preTransQual match { case PreTransLocalDef(LocalDef(_, _, InlineClassBeingConstructedReplacement(_, fieldLocalDefs, cancelFun))) => @@ -1257,6 +1253,47 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } } + private def pretransformAssign(tlhs: PreTransform, trhs: PreTransform)( + cont: PreTransCont)(implicit scope: Scope, pos: Position): TailRec[Tree] = { + def contAssign(lhs: Tree, rhs: Tree) = + cont(PreTransTree(Assign(lhs.asInstanceOf[AssignLhs], rhs))) + + resolveLocalDef(tlhs) match { + case PreTransRecordTree(lhsTree, lhsOrigType, lhsCancelFun) => + val recordType = lhsTree.tpe.asInstanceOf[RecordType] + + def buildInner(trhs: PreTransform): TailRec[Tree] = { + resolveLocalDef(trhs) match { + case PreTransRecordTree(rhsTree, rhsOrigType, rhsCancelFun) => + if (rhsTree.tpe != recordType || rhsOrigType != lhsOrigType) + lhsCancelFun() + contAssign(lhsTree, rhsTree) + case _ => + lhsCancelFun() + } + } + + (trhs.tpe.base, lhsOrigType) match { + case (LongType, RefinedType( + ClassType(LongImpl.RuntimeLongClass), true, false)) => + /* The lhs is a stack-allocated RuntimeLong, but the rhs is + * a primitive Long. We expand the primitive Long into a + * new stack-allocated RuntimeLong so that we do not need + * to cancel. + */ + expandLongValue(trhs) { expandedRhs => + buildInner(expandedRhs) + } + + case _ => + buildInner(trhs) + } + + case PreTransTree(lhsTree, _) => + contAssign(lhsTree, finishTransformExpr(trhs)) + } + } + private def pretransformNew(allocationSite: AllocationSite, className: ClassName, ctor: MethodIdent, targs: List[PreTransform])( cont: PreTransCont)( @@ -2223,13 +2260,19 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { assert(isStat, "Found Assign in expression position") assert(optReceiver.isDefined, "There was a This(), there should be a receiver") - pretransformSelectCommon(lhs.tpe, optReceiver.get._2, className, field, - isLhsOfAssign = true) { preTransLhs => - // TODO Support assignment of record - cont(PreTransTree( - Assign(finishTransformExpr(preTransLhs).asInstanceOf[AssignLhs], - finishTransformExpr(args.head)), - RefinedType.NoRefinedType)) + + val treceiver = optReceiver.get._2 + val trhs = args.head + + if (!isFieldRead(className, field.name)) { + // Field is never read, discard assign, keep side effects only. + cont(PreTransTree(Block(finishTransformStat(treceiver), + finishTransformStat(trhs)))) + } else { + pretransformSelectCommon(lhs.tpe, treceiver, className, field, + isLhsOfAssign = true) { tlhs => + pretransformAssign(tlhs, args.head)(cont) + } } case _ => @@ -2630,11 +2673,37 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { buildInner: (Map[FieldID, LocalDef], PreTransCont) => TailRec[Tree])( cont: PreTransCont)( implicit scope: Scope): TailRec[Tree] = { + + def withStat(stat: Tree, rest: List[Tree]): TailRec[Tree] = { + val transformedStat = transformStat(stat) + transformedStat match { + case Skip() => + inlineClassConstructorBodyList(allocationSite, structure, + thisLocalDef, inputFieldsLocalDefs, + className, rest, cancelFun)(buildInner)(cont) + case _ => + if (transformedStat.tpe == NothingType) + cont(PreTransTree(transformedStat, RefinedType.Nothing)) + else { + inlineClassConstructorBodyList(allocationSite, structure, + thisLocalDef, inputFieldsLocalDefs, + className, rest, cancelFun)(buildInner) { tinner => + cont(PreTransBlock(transformedStat, tinner)) + } + } + } + } + stats match { case This() :: rest => inlineClassConstructorBodyList(allocationSite, structure, thisLocalDef, inputFieldsLocalDefs, className, rest, cancelFun)(buildInner)(cont) + case Assign(s @ Select(ths: This, className, field), value) :: rest + if !inputFieldsLocalDefs.contains(FieldID(className, field)) => + // Field is being optimized away. Only keep side effects of the write. + withStat(value, rest) + case Assign(s @ Select(ths: This, className, field), value) :: rest if !inputFieldsLocalDefs(FieldID(className, field)).mutable => pretransformExpr(value) { tvalue => @@ -2711,23 +2780,7 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { } case stat :: rest => - val transformedStat = transformStat(stat) - transformedStat match { - case Skip() => - inlineClassConstructorBodyList(allocationSite, structure, - thisLocalDef, inputFieldsLocalDefs, - className, rest, cancelFun)(buildInner)(cont) - case _ => - if (transformedStat.tpe == NothingType) - cont(PreTransTree(transformedStat, RefinedType.Nothing)) - else { - inlineClassConstructorBodyList(allocationSite, structure, - thisLocalDef, inputFieldsLocalDefs, - className, rest, cancelFun)(buildInner) { tinner => - cont(PreTransBlock(transformedStat, tinner)) - } - } - } + withStat(stat, rest) case Nil => buildInner(inputFieldsLocalDefs, cont) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala index 50111a07ec..fb34793699 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala @@ -14,7 +14,7 @@ package org.scalajs.linker.standard import org.scalajs.ir.Trees._ import org.scalajs.ir.{ClassKind, Position} -import org.scalajs.ir.Names.ClassName +import org.scalajs.ir.Names.{ClassName, FieldName} /** A ClassDef after linking. * @@ -52,6 +52,8 @@ final class LinkedClass( val hasInstances: Boolean, val hasInstanceTests: Boolean, val hasRuntimeTypeInfo: Boolean, + val fieldsRead: Set[FieldName], + val staticFieldsRead: Set[FieldName], val staticDependencies: Set[ClassName], val externalDependencies: Set[String], @@ -87,6 +89,8 @@ final class LinkedClass( hasInstances: Boolean, hasInstanceTests: Boolean, hasRuntimeTypeInfo: Boolean, + fieldsRead: Set[FieldName], + staticFieldsRead: Set[FieldName], staticDependencies: Set[ClassName], externalDependencies: Set[String], dynamicDependencies: Set[ClassName] @@ -99,6 +103,8 @@ final class LinkedClass( hasInstances = hasInstances, hasInstanceTests = hasInstanceTests, hasRuntimeTypeInfo = hasRuntimeTypeInfo, + fieldsRead = fieldsRead, + staticFieldsRead = staticFieldsRead, staticDependencies = staticDependencies, externalDependencies = externalDependencies, dynamicDependencies = dynamicDependencies @@ -130,6 +136,8 @@ final class LinkedClass( hasInstances: Boolean = this.hasInstances, hasInstanceTests: Boolean = this.hasInstanceTests, hasRuntimeTypeInfo: Boolean = this.hasRuntimeTypeInfo, + fieldsRead: Set[FieldName] = this.fieldsRead, + staticFieldsRead: Set[FieldName] = this.staticFieldsRead, staticDependencies: Set[ClassName] = this.staticDependencies, externalDependencies: Set[String] = this.externalDependencies, dynamicDependencies: Set[ClassName] = this.dynamicDependencies, @@ -153,6 +161,8 @@ final class LinkedClass( hasInstances, hasInstanceTests, hasRuntimeTypeInfo, + fieldsRead, + staticFieldsRead, staticDependencies, externalDependencies, dynamicDependencies, diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 7c91268456..ae5d2a52b7 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 145003, - expectedFullLinkSizeWithoutClosure = 133198, - expectedFullLinkSizeWithClosure = 21338, + expectedFastLinkSize = 141677, + expectedFullLinkSizeWithoutClosure = 129956, + expectedFullLinkSizeWithClosure = 21225, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala index 236df194ed..7415993b39 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala @@ -477,6 +477,87 @@ class OptimizerTest { } } } + + private def commonClassDefsForFieldRemovalTests(classInline: Boolean, + witnessMutable: Boolean): Seq[ClassDef] = { + val methodName = m("method", Nil, I) + + val witnessType = ClassType("Witness") + + val fooMemberDefs = List( + // x: Witness + FieldDef(EMF.withMutable(witnessMutable), "x", NON, witnessType), + + // y: Int + FieldDef(EMF, "y", NON, IntType), + + // def this() = { + // this.x = null + // this.y = 5 + // } + MethodDef(EMF.withNamespace(Constructor), NoArgConstructorName, NON, Nil, NoType, Some(Block( + Assign(Select(This()(ClassType("Foo")), "Foo", "x")(witnessType), Null()), + Assign(Select(This()(ClassType("Foo")), "Foo", "y")(IntType), int(5)) + )))(EOH, None), + + // def method(): Int = this.y + MethodDef(EMF, methodName, NON, Nil, IntType, Some { + Select(This()(ClassType("Foo")), "Foo", "y")(IntType) + })(EOH, None) + ) + + Seq( + classDef("Witness", kind = ClassKind.Interface), + classDef("Foo", kind = ClassKind.Class, superClass = Some(ObjectClass), + memberDefs = fooMemberDefs, optimizerHints = EOH.withInline(classInline)), + mainTestClassDef({ + consoleLog(Apply(EAF, New("Foo", NoArgConstructorName, Nil), methodName, Nil)(IntType)) + }) + ) + } + + @Test + def removeUnusedFields(): AsyncResult = await { + val classDefs = commonClassDefsForFieldRemovalTests(classInline = false, witnessMutable = false) + + for { + moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers) + } yield { + findClass(moduleSet, "Foo").get.fields match { + case List(FieldDef(_, FieldIdent(name), _, _)) if name == FieldName("y") => + // ok + + case fields => + fail(s"Unexpected fields: $fields") + } + + assertFalse(findClass(moduleSet, "Witness").isDefined) + } + } + + @Test + def removeUnusedFieldsInline(): AsyncResult = await { + val classDefs = commonClassDefsForFieldRemovalTests(classInline = true, witnessMutable = false) + + for { + moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers) + } yield { + assertFalse(findClass(moduleSet, "Foo").isDefined) + assertFalse(findClass(moduleSet, "Witness").isDefined) + } + } + + @Test + def removeUnusedMutableFieldsInline(): AsyncResult = await { + val classDefs = commonClassDefsForFieldRemovalTests(classInline = true, witnessMutable = true) + + for { + moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers) + } yield { + assertFalse(findClass(moduleSet, "Foo").isDefined) + assertFalse(findClass(moduleSet, "Witness").isDefined) + } + } } object OptimizerTest { diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 7ac0e840d5..a06de6959e 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -10,8 +10,10 @@ object BinaryIncompatibilities { ) val Linker = Seq( - // Breaking! LinkedClass has one more argument in its constructor - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedClass.this"), + // Breaking (minor change) + exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedClass.this"), + // private[linker], not an issue. + exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedClass.refined"), ) val LinkerInterface = Seq( diff --git a/project/Build.scala b/project/Build.scala index 90c923a9d9..fa2fe85073 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1713,26 +1713,26 @@ object Build { scalaVersion.value match { case Default2_11ScalaVersion => Some(ExpectedSizes( - fastLink = 516500 to 517500, - fullLink = 107000 to 108000, - fastLinkGz = 65500 to 66500, - fullLinkGz = 28000 to 29000, + fastLink = 379000 to 380000, + fullLink = 79000 to 80000, + fastLinkGz = 49000 to 50000, + fullLinkGz = 21000 to 22000, )) case Default2_12ScalaVersion => Some(ExpectedSizes( - fastLink = 778000 to 779000, - fullLink = 148000 to 149000, - fastLinkGz = 90000 to 91000, - fullLinkGz = 36000 to 37000, + fastLink = 756000 to 757000, + fullLink = 145000 to 146000, + fastLinkGz = 88000 to 89000, + fullLinkGz = 35000 to 36000, )) case Default2_13ScalaVersion => Some(ExpectedSizes( - fastLink = 727000 to 728000, - fullLink = 155000 to 156000, - fastLinkGz = 91000 to 92000, - fullLinkGz = 39000 to 40000, + fastLink = 443000 to 444000, + fullLink = 97000 to 98000, + fastLinkGz = 57000 to 58000, + fullLinkGz = 26000 to 27000, )) case _ => diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala index f7ea669d8f..2e8878cef9 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala @@ -610,6 +610,66 @@ class OptimizerTest { } } + @Test def keepQualifierSideEffectsOfEliminatedField(): Unit = { + @noinline + class Foo(var x: Int) + + val foo = new Foo(2) + + var called = false + + @noinline + def getFoo() = { + called = true + foo + } + + getFoo().x = 1 + + assertTrue(called) + } + + @Test def keepQualifierSideEffectsOfEliminatedFieldInline(): Unit = { + @inline + class Foo(var x: Int) + + val foo = new Foo(2) + + var called = false + + @inline + def getFoo() = { + called = true + foo + } + + getFoo().x = 1 + + assertTrue(called) + } + + @Test def keepQualifierSideEffectsOfEliminatedJSField(): Unit = { + class Foo extends js.Object { + private[this] var x: Int = 1 + + @inline + final private[OptimizerTest] def set() = { + x = 2 + } + } + + val foo = new Foo + var called = false + + def getFoo() = { + called = true + foo + } + + getFoo().set() + + assertTrue(called) + } } object OptimizerTest { From 6e3a90a0fa06493019ad63f78de1f4e735f084cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 28 Aug 2022 12:55:16 +0200 Subject: [PATCH 50/75] Add tests for stack traces of JavaScript exceptions. `js.JavaScriptException` must acquire the stack trace of its underlying JavaScript exception. --- .../testsuite/library/StackTraceTest.scala | 84 ++++++++++++------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala index 23a59a8278..eb0b7b2f94 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala @@ -24,13 +24,14 @@ import org.scalajs.testsuite.utils.Platform._ class StackTraceTest { import StackTraceTest._ + @noinline private def verifyClassMethodNames( places: (String, String)*)(body: => Any): Unit = { try { body throw new AssertionError("body should have thrown an exception") } catch { - case e: IllegalArgumentException => + case e: RuntimeException => // common superclass of IllegalArgumentException and js.JavaScriptException val trace = e.getStackTrace() for ((className, methodName) <- places) { val found = trace.exists { elem => @@ -55,40 +56,57 @@ class StackTraceTest { val Error = js.constructorOf[js.Error] val oldStackTraceLimit = Error.stackTraceLimit + val oldThrowJSError = throwJSError Error.stackTraceLimit = 20 try { - verifyClassMethodNames("Foo" -> "f") { - new Foo().f(25) - } + for (jsError <- List(false, true)) { + throwJSError = jsError - verifyClassMethodNames("Foo" -> "f", "Bar" -> "g") { - new Bar().g(7) - } + verifyClassMethodNames("Foo" -> "f") { + new Foo().f(25) + } - verifyClassMethodNames("Foo" -> "f", "FooTrait" -> "h") { - new Foo().h(78) - } + verifyClassMethodNames("Foo" -> "f", "Bar" -> "g") { + new Bar().g(7) + } - verifyClassMethodNames("Foo" -> "f", "FooTrait" -> "h", - "Baz" -> "") { - new Baz() - } + verifyClassMethodNames("Foo" -> "f", "FooTrait" -> "h") { + new Foo().h(78) + } - verifyClassMethodNames("Foo" -> "f", "Bar" -> "g", - "Foobar$" -> "", "Foobar$" -> "") { - Foobar.z - } + verifyClassMethodNames("Foo" -> "f", "FooTrait" -> "h", + "Baz" -> "") { + new Baz() + } - verifyClassMethodNames( - "Foo" -> "f", - "SJS" -> "m", // Scala method actually implementing m() - "SJS" -> "n" // Exported JS method forwarding to m() - ) { - new SJS().m() + /* For the test with a module initializer, we must use a different + * module in each iteration, because exceptions happening during the + * module initializer put their module in a corrupted state. + */ + if (!jsError) { + verifyClassMethodNames("Foo" -> "f", "Bar" -> "g", + "Foobar1$" -> "", "Foobar1$" -> "") { + Foobar1.z + } + } else { + verifyClassMethodNames("Foo" -> "f", "Bar" -> "g", + "Foobar2$" -> "", "Foobar2$" -> "") { + Foobar2.z + } + } + + verifyClassMethodNames( + "Foo" -> "f", + "SJS" -> "m", // Scala method actually implementing m() + "SJS" -> "n" // Exported JS method forwarding to m() + ) { + new SJS().m() + } } } finally { Error.stackTraceLimit = oldStackTraceLimit + throwJSError = oldThrowJSError } } @@ -96,6 +114,8 @@ class StackTraceTest { object StackTraceTest { + var throwJSError: Boolean = false + trait FooTrait { def f(x: Int): Int @@ -106,10 +126,14 @@ object StackTraceTest { class Foo extends FooTrait { @noinline def f(x: Int): Int = { - if (x > 10) - throw new IllegalArgumentException(x.toString) - else + if (x > 10) { + if (throwJSError) + js.special.`throw`(new js.Error(x.toString())) + else + throw new IllegalArgumentException(x.toString) + } else { x + 4 + } } } @@ -122,7 +146,11 @@ object StackTraceTest { val z = new Foo().h(50) } - object Foobar { + object Foobar1 { + val z = new Bar().g(7) + } + + object Foobar2 { val z = new Bar().g(7) } From 88b224cda68e9d346c14785876f1c476667e968a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 28 Aug 2022 13:07:29 +0200 Subject: [PATCH 51/75] Merge the two line regexes in StackTrace.normalizedLinesToStackTrace. We now use a single regex for the cases with and without a column number in the stack line. --- .../src/main/scala/java/lang/StackTrace.scala | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/javalib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala index 0316c5c679..8d252ed0fc 100644 --- a/javalib/src/main/scala/java/lang/StackTrace.scala +++ b/javalib/src/main/scala/java/lang/StackTrace.scala @@ -112,8 +112,7 @@ private[lang] object StackTrace { private def normalizedLinesToStackTrace( lines: js.Array[String]): Array[StackTraceElement] = { - val NormalizedFrameLine = """^([^\@]*)\@(.*):([0-9]+)$""".re - val NormalizedFrameLineWithColumn = """^([^\@]*)\@(.*):([0-9]+):([0-9]+)$""".re + val NormalizedFrameLine = """^([^@]*)@(.*?):([0-9]+)(?::([0-9]+))?$""".re @inline def parseInt(s: String): Int = js.Dynamic.global.parseInt(s).asInstanceOf[Int] @@ -123,27 +122,18 @@ private[lang] object StackTrace { while (i < lines.length) { val line = lines(i) if (!line.isEmpty) { - val mtch1 = NormalizedFrameLineWithColumn.exec(line) - if (mtch1 ne null) { + val mtch = NormalizedFrameLine.exec(line) + if (mtch ne null) { val classAndMethodName = - extractClassMethod(undefOrForceGet(mtch1(1))) + extractClassMethod(undefOrForceGet(mtch(1))) val elem = new StackTraceElement(classAndMethodName(0), - classAndMethodName(1), undefOrForceGet(mtch1(2)), - parseInt(undefOrForceGet(mtch1(3)))) - elem.setColumnNumber(parseInt(undefOrForceGet(mtch1(4)))) + classAndMethodName(1), undefOrForceGet(mtch(2)), + parseInt(undefOrForceGet(mtch(3)))) + undefOrForeach(mtch(4))(c => elem.setColumnNumber(parseInt(c))) trace.push(elem) } else { - val mtch2 = NormalizedFrameLine.exec(line) - if (mtch2 ne null) { - val classAndMethodName = - extractClassMethod(undefOrForceGet(mtch2(1))) - trace.push(new StackTraceElement(classAndMethodName(0), - classAndMethodName(1), undefOrForceGet(mtch2(2)), - parseInt(undefOrForceGet(mtch2(3))))) - } else { - // just in case - trace.push(new StackTraceElement("", line, null, -1)) - } + // just in case + trace.push(new StackTraceElement("", line, null, -1)) } } i += 1 From d158f3c8f381a904336753355e0d2f09c6470e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 14 Aug 2022 12:56:43 +0200 Subject: [PATCH 52/75] Integrate the JavaScriptException extraction for stack traces in StackTrace. When we capture the stack trace of a `js.JavaScriptException`, we want to get the stack trace of the underlying JS error, instead of the point of instantiation of the `js.javaScriptException`. Previously, we did this in an override of `fillInStackTrace()`, using an internal API accessed via reflection. This was not nice, but we had no choice because `j.l.Throwable` and `j.l.StackTrace` could not unwrap a `js.JavaScriptException` without accessing Scala.js-specific stuff. Now, that we have `js.special.unwrapFromThrowable`, we can directly use that in `StackTrace`. In the process, we simplify the entire logic so that `StackTrace` doesn't have to manipulate the state of `Throwable` itself. In turn, this means that no one uses the `{get,set}StackTraceStateInternal` methods anymore, and therefore we remove them. --- .../src/main/scala/java/lang/StackTrace.scala | 81 +++++++++---------- .../src/main/scala/java/lang/Throwables.scala | 18 +---- .../scalajs/js/JavaScriptException.scala | 8 -- .../scalajs/js/JavaScriptException.scala | 8 -- .../org/scalajs/linker/LibrarySizeTest.scala | 2 +- project/BinaryIncompatibilities.scala | 2 + project/Build.scala | 2 +- 7 files changed, 44 insertions(+), 77 deletions(-) diff --git a/javalib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala index 8d252ed0fc..e04960550a 100644 --- a/javalib/src/main/scala/java/lang/StackTrace.scala +++ b/javalib/src/main/scala/java/lang/StackTrace.scala @@ -24,79 +24,72 @@ import Utils._ private[lang] object StackTrace { /* !!! Note that in this unit, we go to great lengths *not* to use anything - * from the Scala collections library. + * from the collections library, and in general to use as little non-JS APIs + * as possible. * - * This minimizes the risk of runtime errors during the process of decoding + * This minimizes the risk of run-time errors during the process of decoding * errors, which would be very bad if it happened. */ /** Returns the current stack trace. - * If the stack trace cannot be analyzed in meaningful way (because we don't - * know the browser), an empty array is returned. + * + * If the stack trace cannot be analyzed in a meaningful way (normally, + * only in case we don't know the engine's format for stack traces), an + * empty array is returned. */ def getCurrentStackTrace(): Array[StackTraceElement] = - extract(createException().asInstanceOf[js.Dynamic]) + extract(new js.Error()) - /** Captures browser-specific state recording the current stack trace. + /** Captures a JavaScript error object recording the stack trace of the given + * `Throwable`. + * * The state is stored as a magic field of the throwable, and will be used * by `extract()` to create an Array[StackTraceElement]. */ - @inline def captureState(throwable: Throwable): Unit = { - val throwableAsJSAny = throwable.asInstanceOf[js.Any] + @inline def captureJSError(throwable: Throwable): Any = { + val reference = js.special.unwrapFromThrowable(throwable) val identifyingString: Any = { js.constructorOf[js.Object].prototype .selectDynamic("toString") - .call(throwableAsJSAny) + .call(reference.asInstanceOf[js.Any]) } if ("[object Error]" == identifyingString) { - /* The `throwable` has an `[[ErrorData]]` internal slot, which is as good + /* The `reference` has an `[[ErrorData]]` internal slot, which is as good * a guarantee as any that it contains stack trace data itself. In * practice, this happens when we emit ES 2015 classes, and no other * compiler down the line has compiled them away as ES 5.1 functions and * prototypes. */ - captureState(throwable, throwable) + reference } else if (js.constructorOf[js.Error].captureStackTrace eq ().asInstanceOf[AnyRef]) { - captureState(throwable, createException()) + // Create a JS Error with the current stack trace. + new js.Error() } else { /* V8-specific. - * The Error.captureStackTrace(e) method records the current stack trace - * on `e` as would do `new Error()`, thereby turning `e` into a proper - * exception. This avoids creating a dummy exception, but is mostly - * important so that Node.js will show stack traces if the exception - * is never caught and reaches the global event queue. + * + * The `Error.captureStackTrace(e)` method records the current stack + * trace on `e` as would do `new Error()`, thereby turning `e` into a + * proper exception. This avoids creating a dummy exception, but is + * mostly important so that Node.js will show stack traces if the + * exception is never caught and reaches the global event queue. + * + * We use the `throwable` itself instead of the `reference` in this case, + * since the latter is not under our control, and could even be a + * primitive value which cannot be passed to `captureStackTrace`. */ - js.constructorOf[js.Error].captureStackTrace(throwableAsJSAny) - captureState(throwable, throwable) + js.constructorOf[js.Error].captureStackTrace(throwable.asInstanceOf[js.Any]) + throwable } } - /** Creates a JS Error with the current stack trace state. */ - @inline private def createException(): Any = - new js.Error() - - /** Captures browser-specific state recording the stack trace of a JS error. - * The state is stored as a magic field of the throwable, and will be used - * by `extract()` to create an Array[StackTraceElement]. - */ - @inline def captureState(throwable: Throwable, e: Any): Unit = - throwable.setStackTraceStateInternal(e) - - /** Extracts a throwable's stack trace from captured browser-specific state. - * If no stack trace state has been recorded, or if the state cannot be - * analyzed in meaningful way (because we don't know the browser), an - * empty array is returned. - */ - def extract(throwable: Throwable): Array[StackTraceElement] = - extract(throwable.getStackTraceStateInternal()) - - /** Extracts a stack trace from captured browser-specific stackdata. - * If no stack trace state has been recorded, or if the state cannot be - * analyzed in meaningful way (because we don't know the browser), an - * empty array is returned. + /** Extracts a stack trace from a JavaScript error object. + * If the provided error is not a JavaScript object, or if its stack data + * otherwise cannot be analyzed in a meaningful way (normally, only in case + * we don't know the engine's format for stack traces), an empty array is + * returned. */ - private def extract(stackdata: Any): Array[StackTraceElement] = { - val lines = normalizeStackTraceLines(stackdata.asInstanceOf[js.Dynamic]) + def extract(jsError: Any): Array[StackTraceElement] = { + val lines = normalizeStackTraceLines(jsError.asInstanceOf[js.Dynamic]) normalizedLinesToStackTrace(lines) } diff --git a/javalib/src/main/scala/java/lang/Throwables.scala b/javalib/src/main/scala/java/lang/Throwables.scala index 87484ff2ec..38b17384b9 100644 --- a/javalib/src/main/scala/java/lang/Throwables.scala +++ b/javalib/src/main/scala/java/lang/Throwables.scala @@ -24,7 +24,7 @@ class Throwable protected (s: String, private var e: Throwable, def this(s: String) = this(s, null) def this(e: Throwable) = this(if (e == null) null else e.toString, e) - private[this] var stackTraceStateInternal: Any = _ + private[this] var jsErrorForStackTrace: Any = _ private[this] var stackTrace: Array[StackTraceElement] = _ /* We use an Array rather than, say, a List, so that Throwable does not @@ -45,26 +45,14 @@ class Throwable protected (s: String, private var e: Throwable, def getLocalizedMessage(): String = getMessage() def fillInStackTrace(): Throwable = { - StackTrace.captureState(this) + jsErrorForStackTrace = StackTrace.captureJSError(this) this } - /* Not part of the JDK API, used internally in java.lang and accessible - * through reflection. - */ - def getStackTraceStateInternal(): Any = - stackTraceStateInternal - - /* Not part of the JDK API, used internally in java.lang and accessible - * through reflection. - */ - def setStackTraceStateInternal(e: Any): Unit = - stackTraceStateInternal = e - def getStackTrace(): Array[StackTraceElement] = { if (stackTrace eq null) { if (writableStackTrace) - stackTrace = StackTrace.extract(this) + stackTrace = StackTrace.extract(jsErrorForStackTrace) else stackTrace = new Array[StackTraceElement](0) } diff --git a/library/src/main/scala/scala/scalajs/js/JavaScriptException.scala b/library/src/main/scala/scala/scalajs/js/JavaScriptException.scala index f9ebd4a779..302d7ca5ca 100644 --- a/library/src/main/scala/scala/scalajs/js/JavaScriptException.scala +++ b/library/src/main/scala/scala/scalajs/js/JavaScriptException.scala @@ -20,12 +20,4 @@ final case class JavaScriptException(exception: scala.Any) extends RuntimeException { override def getMessage(): String = exception.toString() - - override def fillInStackTrace(): Throwable = { - type JSExceptionEx = JavaScriptException { - def setStackTraceStateInternal(e: scala.Any): Unit - } - this.asInstanceOf[JSExceptionEx].setStackTraceStateInternal(exception) - this - } } diff --git a/linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala b/linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala index 609a2dedfa..68c4321b8e 100644 --- a/linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala +++ b/linker-private-library/src/main/scala/scala/scalajs/js/JavaScriptException.scala @@ -18,12 +18,4 @@ final class JavaScriptException(val exception: scala.Any) extends RuntimeException { override def getMessage(): String = exception.toString() - - override def fillInStackTrace(): Throwable = { - type JSExceptionEx = JavaScriptException { - def setStackTraceStateInternal(e: scala.Any): Unit - } - this.asInstanceOf[JSExceptionEx].setStackTraceStateInternal(exception) - this - } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index ae5d2a52b7..c9c5290581 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,7 +70,7 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 141677, + expectedFastLinkSize = 142501, expectedFullLinkSizeWithoutClosure = 129956, expectedFullLinkSizeWithClosure = 21225, classDefs, diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index a06de6959e..fdf5c4ee56 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -28,6 +28,8 @@ object BinaryIncompatibilities { ) val Library = Seq( + // Static initializer (2.11 only), not an issue + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.scalajs.js.JavaScriptException."), ) val TestInterface = Seq( diff --git a/project/Build.scala b/project/Build.scala index fa2fe85073..6f2131e2cc 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1713,7 +1713,7 @@ object Build { scalaVersion.value match { case Default2_11ScalaVersion => Some(ExpectedSizes( - fastLink = 379000 to 380000, + fastLink = 380000 to 381000, fullLink = 79000 to 80000, fastLinkGz = 49000 to 50000, fullLinkGz = 21000 to 22000, From 9dee4919857f37d9abb02844fff7ebd79a445844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 14 Aug 2022 14:17:38 +0200 Subject: [PATCH 53/75] Take the columnNumber in the constructor of `StackTraceElement`. Historically, we had to use a reflection-based method to set the column number, because `StackTrace.scala` was compiled separately from `StackTraceElement.scala`. Unfortunately, that forced `StackTraceElement` to be mutable whereas it is not supposed to be. Now, we instead have an additional constructor that directly takes the column number. We use it in `StackTrace`, and depreate the method `setColumnNumber`. The deprecation is symbolic, since that method does not appear in any binary API. It remains reachable using reflection, which is why we do not remove it outright. Because of that, `StackTraceElement` is still effectively mutable, but we have a path to make truly immutable in the future. --- .../src/main/scala/java/lang/StackTrace.scala | 10 +++++----- .../scala/java/lang/StackTraceElement.scala | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/javalib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala index e04960550a..4dac37591c 100644 --- a/javalib/src/main/scala/java/lang/StackTrace.scala +++ b/javalib/src/main/scala/java/lang/StackTrace.scala @@ -119,14 +119,14 @@ private[lang] object StackTrace { if (mtch ne null) { val classAndMethodName = extractClassMethod(undefOrForceGet(mtch(1))) - val elem = new StackTraceElement(classAndMethodName(0), + trace.push(new StackTraceElement(classAndMethodName(0), classAndMethodName(1), undefOrForceGet(mtch(2)), - parseInt(undefOrForceGet(mtch(3)))) - undefOrForeach(mtch(4))(c => elem.setColumnNumber(parseInt(c))) - trace.push(elem) + parseInt(undefOrForceGet(mtch(3))), + undefOrFold(mtch(4))(-1)(parseInt(_)))) } else { // just in case - trace.push(new StackTraceElement("", line, null, -1)) + // (explicitly use the constructor with column number so that STE has an inlineable init) + trace.push(new StackTraceElement("", line, null, -1, -1)) } } i += 1 diff --git a/javalib/src/main/scala/java/lang/StackTraceElement.scala b/javalib/src/main/scala/java/lang/StackTraceElement.scala index d9e5f81274..f018fd2776 100644 --- a/javalib/src/main/scala/java/lang/StackTraceElement.scala +++ b/javalib/src/main/scala/java/lang/StackTraceElement.scala @@ -15,10 +15,16 @@ package java.lang import scala.scalajs.js import js.annotation.JSExport +/* The primary constructor, taking a `columnNumber`, is not part of the JDK + * API. It is used internally in `java.lang.StackTrace`, and could be accessed + * by third-party libraries with a bit of IR manipulation. + */ final class StackTraceElement(declaringClass: String, methodName: String, - fileName: String, lineNumber: Int) extends AnyRef with java.io.Serializable { + fileName: String, lineNumber: Int, private[this] var columnNumber: Int) + extends AnyRef with java.io.Serializable { - private[this] var columnNumber: Int = -1 + def this(declaringClass: String, methodName: String, fileName: String, lineNumber: Int) = + this(declaringClass, methodName, fileName, lineNumber, -1) def getFileName(): String = fileName def getLineNumber(): Int = lineNumber @@ -26,14 +32,11 @@ final class StackTraceElement(declaringClass: String, methodName: String, def getMethodName(): String = methodName def isNativeMethod(): scala.Boolean = false - /* Not part of the JDK API, used internally in java.lang and accessible - * through reflection. - */ + // Not part of the JDK API, accessible through reflection. def getColumnNumber(): Int = columnNumber - /* Not part of the JDK API, used internally in java.lang and accessible - * through reflection. - */ + // Not part of the JDK API, accessible through reflection. + @deprecated("old internal API; use the constructor with a column number instead", "1.11.0") def setColumnNumber(columnNumber: Int): Unit = this.columnNumber = columnNumber From 20da193539bee02cfdc9d500f20bc9c0744427a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 14 Aug 2022 14:28:29 +0200 Subject: [PATCH 54/75] Fix StackTraceElement.{equals,hashCode}. They were not taking all its components into account. --- javalib/src/main/scala/java/lang/StackTraceElement.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/javalib/src/main/scala/java/lang/StackTraceElement.scala b/javalib/src/main/scala/java/lang/StackTraceElement.scala index f018fd2776..8795a1de82 100644 --- a/javalib/src/main/scala/java/lang/StackTraceElement.scala +++ b/javalib/src/main/scala/java/lang/StackTraceElement.scala @@ -44,6 +44,7 @@ final class StackTraceElement(declaringClass: String, methodName: String, case that: StackTraceElement => (getFileName() == that.getFileName()) && (getLineNumber() == that.getLineNumber()) && + (getColumnNumber() == that.getColumnNumber()) && (getClassName() == that.getClassName()) && (getMethodName() == that.getMethodName()) case _ => @@ -73,6 +74,10 @@ final class StackTraceElement(declaringClass: String, methodName: String, } override def hashCode(): Int = { - declaringClass.hashCode() ^ methodName.hashCode() + declaringClass.hashCode() ^ + methodName.hashCode() ^ + fileName.hashCode() ^ + lineNumber ^ + columnNumber } } From 43acc6a48aff52ebae3c601b1a44f24fb64ee639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 30 Aug 2022 16:07:54 +0200 Subject: [PATCH 55/75] Fix #4716: Avoid duplicate capture param defs caused by aliasing. When two different vals captured by the same closure end up being aliases of each other, the optimizer would rename the two capture params to have the same name, which caused duplicate parameter definitions. We now record which replacement names have already been assigned to a capture param local def. If the same name appears a second time, we directly reuse the previous local def instead of adding another capture param. --- .../frontend/optimizer/OptimizerCore.scala | 15 ++++++++-- .../testsuite/compiler/RegressionTest.scala | 28 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 2ba19df2ec..454703d9d0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -736,13 +736,14 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { val captureParamLocalDefs = List.newBuilder[(LocalName, LocalDef)] val newCaptureParamDefsAndRepls = List.newBuilder[(ParamDef, ReplaceWithVarRef)] val captureValueBindings = List.newBuilder[Binding] + val captureParamLocalDefsForVarRefs = mutable.Map.empty[LocalName, LocalDef] for ((paramDef, tcaptureValue) <- captureParams.zip(tcaptureValues)) { val ParamDef(ident @ LocalIdent(paramName), originalName, ptpe, mutable) = paramDef assert(!mutable, s"Found mutable capture at ${paramDef.pos}") - def addCaptureParam(newName: LocalName): Unit = { + def addCaptureParam(newName: LocalName): LocalDef = { val newOriginalName = originalNameForFresh(paramName, originalName, newName) val replacement = ReplaceWithVarRef(newName, newSimpleState(Unused), None) @@ -758,6 +759,8 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { captureParamLocalDefs += paramName -> localDef newCaptureParamDefsAndRepls += newParamDef -> replacement captureValueBindings += valueBinding + + localDef } tcaptureValue match { @@ -765,7 +768,15 @@ private[optimizer] abstract class OptimizerCore(config: CommonPhaseConfig) { captureParamLocalDefs += paramName -> LocalDef(tcaptureValue.tpe, false, ReplaceWithConstant(literal)) case PreTransLocalDef(LocalDef(_, /* mutable = */ false, ReplaceWithVarRef(captureName, _, _))) => - addCaptureParam(captureName) + captureParamLocalDefsForVarRefs.get(captureName).fold[Unit] { + captureParamLocalDefsForVarRefs += captureName -> addCaptureParam(captureName) + } { prevLocalDef => + /* #4716 Two capture values may have been aliased to the same VarRef. + * They must use the same capture param LocalDef, otherwise we will + * create duplicate capture params. + */ + captureParamLocalDefs += paramName -> prevLocalDef + } case _ => addCaptureParam(freshLocalNameWithoutOriginalName(paramName, mutable)) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala index 2f189e9b65..2886f742f0 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala @@ -935,6 +935,34 @@ class RegressionTest { assertEquals(8, foo.bar(2)) } + @Test + def valueCapturedTwiceWithDifferentNames_Issue4716(): Unit = { + /* The optimizer used to produce Closures with duplicate capture parameter + * names. This happens when two different vals are captured in a lambda, + * and these vals are aliases of each other so the optimizer merges them. + * It then gives the same name to the capture params. + * + * To reproduce the bug, we need captures that cannot be eliminated by the + * emitter afterwards. This is why we need the loop. + */ + + @noinline def hideClosure[A](f: () => A): A = f() + + var done = false + while (!done) { // don't remove this loop or the test becomes moot + @noinline def makePair(): (Int, Int) = (5, 6) + + val capture1 = makePair() + val capture2: scala.Product2[Int, Int] = capture1 + + assertEquals(11, hideClosure { () => + capture1._1 + capture2._2 + }) + + done = true + } + } + } object RegressionTest { From a76ccff627fdebecfa75b97b940e8b9bb0c8568b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 30 Aug 2022 16:26:12 +0200 Subject: [PATCH 56/75] Don't `clean` at every step of the CI script. Historically, we have `clean`ed the projects between every CI script because we wanted to force re-linking after changing settings. This is not necessary anymore, because changing settings triggers re-linking anyway. Cleaning has the drawback of forcing re-*compilation* as well, which is a lot of time for the test suite. Therefore, we do not `clean` anymore between any two individual steps. However, we make sure to full-clean everything when we retry the entire the job. --- Jenkinsfile | 111 ++++++++++++++++++---------------------------------- 1 file changed, 38 insertions(+), 73 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 407ca8d84c..3c12e10e2d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -108,23 +108,18 @@ def Tasks = [ sbtretry ++$scala helloworld$v/run && sbtretry 'set scalaJSStage in Global := FullOptStage' \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withPrettyPrint(true))' \ - ++$scala helloworld$v/run \ - helloworld$v/clean && + ++$scala helloworld$v/run && sbtretry 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withOptimizer(false))' \ - ++$scala helloworld$v/run \ - helloworld$v/clean && + ++$scala helloworld$v/run && sbtretry 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withSemantics(_.withAsInstanceOfs(CheckedBehavior.Unchecked)))' \ - ++$scala helloworld$v/run \ - helloworld$v/clean && + ++$scala helloworld$v/run && sbtretry ++$scala \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ - helloworld$v/run \ - helloworld$v/clean && + helloworld$v/run && sbtretry ++$scala \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ - helloworld$v/run \ - helloworld$v/clean && + helloworld$v/run && sbtretry ++$scala \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ helloworld$v/run && @@ -139,18 +134,15 @@ def Tasks = [ sbtretry ++$scala \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ 'set scalaJSStage in Global := FullOptStage' \ - helloworld$v/run \ - helloworld$v/clean && + helloworld$v/run && sbtretry ++$scala testingExample$v/testHtmlJSDom && sbtretry ++$scala \ 'set scalaJSLinkerConfig in testingExample.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ 'set scalaJSLinkerConfig in testingExample.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ - testingExample$v/testHtml \ - testingExample$v/clean && + testingExample$v/testHtml && sbtretry 'set scalaJSStage in Global := FullOptStage' \ - ++$scala testingExample$v/testHtmlJSDom \ - testingExample$v/clean && - sbtretry ++$scala testSuiteJVM$v/test testSuiteJVM$v/clean testSuiteExJVM$v/test testSuiteExJVM$v/clean && + ++$scala testingExample$v/testHtmlJSDom && + sbtretry ++$scala testSuiteJVM$v/test testSuiteExJVM$v/test && sbtretry ++$scala testSuite$v/test && sbtretry ++$scala testSuiteEx$v/test && sbtretry 'set scalaJSStage in Global := FullOptStage' \ @@ -164,14 +156,12 @@ def Tasks = [ 'set scalaJSLinkerConfig in reversi.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ 'set scalaJSLinkerConfig in reversi.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ reversi$v/fastLinkJS \ - reversi$v/fullLinkJS \ - reversi$v/clean && + reversi$v/fullLinkJS && sbtretry ++$scala \ 'set scalaJSLinkerConfig in reversi.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("reversi"))))' \ 'set scalaJSLinkerConfig in reversi.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ reversi$v/fastLinkJS \ - reversi$v/fullLinkJS \ - reversi$v/clean && + reversi$v/fullLinkJS && sbtretry ++$scala \ reversi$v/fastLinkJS \ reversi$v/fullLinkJS \ @@ -194,48 +184,39 @@ def Tasks = [ sbtretry ++$scala $testSuite$v/test $testSuite$v/testHtmlJSDom && sbtretry 'set scalaJSStage in Global := FullOptStage' \ ++$scala $testSuite$v/test \ - $testSuite$v/testHtmlJSDom \ - $testSuite$v/clean && + $testSuite$v/testHtmlJSDom && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)).withOptimizer(false))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAvoidLetsAndConsts(false).withAvoidClasses(false)))' \ ++$scala $testSuite$v/test && sbtretry \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAvoidLetsAndConsts(false).withAvoidClasses(false)))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalacOptions in $testSuite.v$v += "-Xexperimental"' \ ++$scala $testSuite$v/test && sbtretry 'set scalacOptions in $testSuite.v$v += "-Xexperimental"' \ @@ -249,8 +230,7 @@ def Tasks = [ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ ++$scala $testSuite$v/test && @@ -277,13 +257,11 @@ def Tasks = [ 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 scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$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 ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$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 ~= makeCompliant' \ @@ -292,14 +270,12 @@ 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 scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$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 ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$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)) }' \ @@ -308,14 +284,12 @@ 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 ~= { _.withSemantics(_.withStrictFloats(false)) }' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$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 \ - $testSuite$v/clean + ++$scala $testSuite$v/test ''', "test-suite-custom-esversion": ''' @@ -325,48 +299,40 @@ def Tasks = [ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ '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 ~= (_.withOptimizer(false))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry '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))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$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 \ - $testSuite$v/clean && + ++$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 \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion).withAllowBigIntsForLongs(true)))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion).withAllowBigIntsForLongs(true)).withOptimizer(false))' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ ++$scala $testSuite$v/test && @@ -378,8 +344,7 @@ def Tasks = [ sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ - $testSuite$v/clean && + ++$scala $testSuite$v/test && sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withESVersion(ESVersion.$esVersion)))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ ++$scala $testSuite$v/test && @@ -593,9 +558,9 @@ matrix.each { taskDef -> buildDefs.put(fullTaskName, { node('linuxworker') { checkout scm - sh "git clean -fdx && rm -rf partest/fetchedSources/" - writeFile file: 'ciscript.sh', text: ciScript, encoding: 'UTF-8' retry(2) { + sh "git clean -fdx && rm -rf partest/fetchedSources/" + writeFile file: 'ciscript.sh', text: ciScript, encoding: 'UTF-8' timeout(time: 4, unit: 'HOURS') { sh "echo '$fullTaskName' && cat ciscript.sh && sh ciscript.sh" } From d75084251b5b6609330b5e7e5fd221be220b8ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Sep 2022 11:32:53 +0200 Subject: [PATCH 57/75] Address a new deprecation warning in Scala 2.13.9. In the compiler, using a `Setting[Boolean]` as a `Boolean` is now deprecated. We have to explicitly call `.value`. --- compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index c4bab865a7..06e9572fe6 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -445,7 +445,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case ex: InterruptedException => throw ex case ex: Throwable => - if (settings.debug) + if (settings.debug.value) ex.printStackTrace() globalError(s"Error while emitting ${cunit.source}\n${ex.getMessage}") } finally { @@ -1129,7 +1129,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * static forwarders? */ def isCandidateForForwarders(sym: Symbol): Boolean = { - !settings.noForwarders && sym.isStatic && !isImplClass(sym) && { + !settings.noForwarders.value && sym.isStatic && !isImplClass(sym) && { // Reject non-top-level objects unless opted in via the appropriate option scalaJSOpts.genStaticForwardersForNonTopLevelObjects || !sym.name.containsChar('$') // this is the same test that scalac performs From 68aac80c775478ce0cd7e6e4f80973da79f0aba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 4 Sep 2022 11:38:15 +0200 Subject: [PATCH 58/75] Directly wrap ArrayBuffer into Int8Array in TypedArrayBuffer.wrap. Instead of passing the `ArrayBuffer`, `byteOffset` and `length` down a few layers of abstraction before doing the wrapping inside the javalib. This reduces the extended API surface of the javalib. --- .../src/main/scala/java/nio/ByteBuffer.scala | 6 ----- .../scala/java/nio/TypedArrayByteBuffer.scala | 6 ----- .../typedarray/TypedArrayBufferBridge.scala | 6 ----- .../js/typedarray/TypedArrayBuffer.scala | 24 ++++++++++++++----- .../typedarray/TypedArrayBufferBridge.scala | 4 ---- 5 files changed, 18 insertions(+), 28 deletions(-) diff --git a/javalib/src/main/scala/java/nio/ByteBuffer.scala b/javalib/src/main/scala/java/nio/ByteBuffer.scala index 92cb2a8ea0..0c042c0719 100644 --- a/javalib/src/main/scala/java/nio/ByteBuffer.scala +++ b/javalib/src/main/scala/java/nio/ByteBuffer.scala @@ -31,12 +31,6 @@ object ByteBuffer { // Extended API - def wrapArrayBuffer(array: ArrayBuffer): ByteBuffer = - TypedArrayByteBuffer.wrapArrayBuffer(array) - - def wrapArrayBuffer(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = - TypedArrayByteBuffer.wrapArrayBuffer(array, byteOffset, length) - def wrapInt8Array(array: Int8Array): ByteBuffer = TypedArrayByteBuffer.wrapInt8Array(array) } diff --git a/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala b/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala index 2371a887e5..d7c1479f69 100644 --- a/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala +++ b/javalib/src/main/scala/java/nio/TypedArrayByteBuffer.scala @@ -226,12 +226,6 @@ private[nio] object TypedArrayByteBuffer { new TypedArrayByteBuffer(new Int8Array(capacity), 0, capacity, false) } - def wrapArrayBuffer(array: ArrayBuffer): ByteBuffer = - wrapInt8Array(new Int8Array(array)) - - def wrapArrayBuffer(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = - wrapInt8Array(new Int8Array(array, byteOffset, length)) - def wrapInt8Array(typedArray: Int8Array): ByteBuffer = { val buf = new TypedArrayByteBuffer(typedArray, 0, typedArray.length, false) buf._isBigEndian = ByteOrder.areTypedArraysBigEndian diff --git a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala b/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala index 3468a9f7e9..68e92f315f 100644 --- a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala +++ b/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala @@ -41,12 +41,6 @@ package scala.scalajs.js.typedarray import java.nio._ private[typedarray] object TypedArrayBufferBridge { - def wrapArrayBuffer(array: Any): ByteBuffer = - ByteBuffer.wrapArrayBuffer(array.asInstanceOf[ArrayBuffer]) - - def wrapArrayBuffer(array: Any, byteOffset: Int, length: Int): ByteBuffer = - ByteBuffer.wrapArrayBuffer(array.asInstanceOf[ArrayBuffer], byteOffset, length) - def wrapInt8Array(array: Any): ByteBuffer = ByteBuffer.wrapInt8Array(array.asInstanceOf[Int8Array]) diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala index 42e561d39f..b8d32b3b65 100644 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala +++ b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala @@ -22,13 +22,25 @@ import java.nio._ * the native byte order of the platform. */ object TypedArrayBuffer { - /** Wraps an [[ArrayBuffer]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. */ - def wrap(array: ArrayBuffer): ByteBuffer = - TypedArrayBufferBridge.wrapArrayBuffer(array) + /** Wraps an [[ArrayBuffer]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. + * + * Equivalent to + * {{{ + * TypedArrayBuffer.wrap(new Int8Array(buffer)) + * }}} + */ + def wrap(buffer: ArrayBuffer): ByteBuffer = + wrap(new Int8Array(buffer)) - /** Wraps an [[ArrayBuffer]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. */ - def wrap(array: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = - TypedArrayBufferBridge.wrapArrayBuffer(array, byteOffset, length) + /** Wraps a view of an [[ArrayBuffer]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. + * + * Equivalent to + * {{{ + * TypedArrayBuffer.wrap(new Int8Array(buffer, byteOffset, length)) + * }}} + */ + def wrap(buffer: ArrayBuffer, byteOffset: Int, length: Int): ByteBuffer = + wrap(new Int8Array(buffer, byteOffset, length)) /** Wraps an [[Int8Array]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. */ def wrap(array: Int8Array): ByteBuffer = diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala index 857abcadc4..3c0e57d400 100644 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala +++ b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala @@ -41,10 +41,6 @@ package scala.scalajs.js.typedarray import java.nio._ private[typedarray] object TypedArrayBufferBridge { - def wrapArrayBuffer(array: Any): ByteBuffer = stub() - - def wrapArrayBuffer(array: Any, byteOffset: Int, length: Int): ByteBuffer = stub() - def wrapInt8Array(array: Any): ByteBuffer = stub() def wrapUint16Array(array: Any): CharBuffer = stub() From ba8ba87e3e68b72157de311acc5d5a96610da15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Sep 2022 15:02:30 +0200 Subject: [PATCH 59/75] Fix #4710: Specify our javalib extended binary API. We introduce a new library `javalibintf`, which is Java-only and contains no implementation. It only defines the extended binary API of our javalib. There were two areas in which the javalib silently exposed an extended API: TypedArray Buffers, and the column number of StackTraceElements. We now define well-specified APIs as static methods for all these secret APIs. The APIs specified in Java by javalibintf are implemented in Scala.js by the javalib. They are used from the scalajs-library. The `library` depends on `javalibintf` in the Provided scope, so that it is available at compile-time (and in IDEs) but does not transitively appear in clients of the `library`. This is important because the APIs are weakly typed when it comes to JavaScript types such as `TypedArray`s. We do not want regular user to get access to them too easily. The `TypedArrayBuffer` API replaces the need for the `TypedArrayBufferBridge` hack in the build. The `StackTraceElement` API is not currently used anywhere, because we did not have any feature in the `library` using them. So far, only the "reflection" API was specified. We may want to introduce a safer API in the `library` in the future. We do publish the `javalibintf`, as it may be used by other language implementations. It will also be useful to be able to MiMa-check future versions of the `javalibintf`. --- Jenkinsfile | 2 +- build.sbt | 1 + .../javalibintf/StackTraceElement.scala | 26 ++ .../javalibintf/TypedArrayBuffer.scala | 56 ++++ .../typedarray/TypedArrayBufferBridge.scala | 79 ----- .../javalibintf/StackTraceElement.java | 74 ++++ .../scalajs/javalibintf/TypedArrayBuffer.java | 315 ++++++++++++++++++ .../js/typedarray/TypedArrayBuffer.scala | 14 +- .../typedarray/TypedArrayBufferBridge.scala | 70 ---- .../js/typedarray/TypedArrayBufferOps.scala | 14 +- project/Build.scala | 24 +- project/MultiScalaProject.scala | 3 + scripts/publish.sh | 14 +- 13 files changed, 518 insertions(+), 174 deletions(-) create mode 100644 javalib/src/main/scala/org/scalajs/javalibintf/StackTraceElement.scala create mode 100644 javalib/src/main/scala/org/scalajs/javalibintf/TypedArrayBuffer.scala delete mode 100644 javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala create mode 100644 javalibintf/src/main/java/org/scalajs/javalibintf/StackTraceElement.java create mode 100644 javalibintf/src/main/java/org/scalajs/javalibintf/TypedArrayBuffer.java delete mode 100644 library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala diff --git a/Jenkinsfile b/Jenkinsfile index 3c12e10e2d..438cc2767c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -166,7 +166,7 @@ def Tasks = [ reversi$v/fastLinkJS \ reversi$v/fullLinkJS \ reversi$v/checksizes && - sbtretry ++$scala compiler$v/compile:doc library$v/compile:doc \ + sbtretry ++$scala javalibintf/compile:doc compiler$v/compile:doc library$v/compile:doc \ testInterface$v/compile:doc testBridge$v/compile:doc && sbtretry ++$scala headerCheck && sbtretry ++$scala partest$v/fetchScalaSource && diff --git a/build.sbt b/build.sbt index 73bfa4a655..d1ad06c8fc 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,7 @@ val linker = Build.linker val linkerJS = Build.linkerJS val testAdapter = Build.testAdapter val sbtPlugin = Build.plugin +val javalibintf = Build.javalibintf val javalib = Build.javalib val scalalib = Build.scalalib val libraryAux = Build.libraryAux diff --git a/javalib/src/main/scala/org/scalajs/javalibintf/StackTraceElement.scala b/javalib/src/main/scala/org/scalajs/javalibintf/StackTraceElement.scala new file mode 100644 index 0000000000..5237986051 --- /dev/null +++ b/javalib/src/main/scala/org/scalajs/javalibintf/StackTraceElement.scala @@ -0,0 +1,26 @@ +/* + * 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.javalibintf + +import java.{lang => jl} + +object StackTraceElement { + def createWithColumnNumber(declaringClass: String, methodName: String, + fileName: String, lineNumber: Int, columnNumber: Int): jl.StackTraceElement = { + new jl.StackTraceElement(declaringClass, methodName, fileName, + lineNumber, columnNumber) + } + + def getColumnNumber(stackTraceElement: jl.StackTraceElement): Int = + stackTraceElement.getColumnNumber() +} diff --git a/javalib/src/main/scala/org/scalajs/javalibintf/TypedArrayBuffer.scala b/javalib/src/main/scala/org/scalajs/javalibintf/TypedArrayBuffer.scala new file mode 100644 index 0000000000..7fbe4d23c6 --- /dev/null +++ b/javalib/src/main/scala/org/scalajs/javalibintf/TypedArrayBuffer.scala @@ -0,0 +1,56 @@ +/* + * 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.javalibintf + +import java.nio._ + +import scala.scalajs.js.typedarray._ + +object TypedArrayBuffer { + + def wrapInt8Array(array: Any): ByteBuffer = + ByteBuffer.wrapInt8Array(array.asInstanceOf[Int8Array]) + + def wrapUint16Array(array: Any): CharBuffer = + CharBuffer.wrapUint16Array(array.asInstanceOf[Uint16Array]) + + def wrapInt16Array(array: Any): ShortBuffer = + ShortBuffer.wrapInt16Array(array.asInstanceOf[Int16Array]) + + def wrapInt32Array(array: Any): IntBuffer = + IntBuffer.wrapInt32Array(array.asInstanceOf[Int32Array]) + + def wrapFloat32Array(array: Any): FloatBuffer = + FloatBuffer.wrapFloat32Array(array.asInstanceOf[Float32Array]) + + def wrapFloat64Array(array: Any): DoubleBuffer = + DoubleBuffer.wrapFloat64Array(array.asInstanceOf[Float64Array]) + + def hasArrayBuffer(buffer: Buffer): Boolean = + buffer.hasArrayBuffer() + + def arrayBuffer(buffer: Buffer): Any = + buffer.arrayBuffer() + + def arrayBufferOffset(buffer: Buffer): Int = + buffer.arrayBufferOffset() + + def dataView(buffer: Buffer): Any = + buffer.dataView() + + def hasTypedArray(buffer: Buffer): Boolean = + buffer.hasTypedArray() + + def typedArray(buffer: Buffer): Any = + buffer.typedArray() +} diff --git a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala b/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala deleted file mode 100644 index 68e92f315f..0000000000 --- a/javalib/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala +++ /dev/null @@ -1,79 +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. - */ - -/* !!!!! - * THIS FILE IS ALMOST COPY-PASTED IN javalib/ AND library/. - * THEY MUST BE KEPT IN SYNC. - * - * This file acts as bridge for scala.scalajs.js.typedarray to be able to - * access the additional public API provided by java.nio, but which is not - * part of the JDK API. Because javalib/ does not export its .class files, - * we cannot call this additional API directly from library/, even though the - * members are public. - * - * In library/, this file has only the signatures, with stub implementations. - * In javalib/, it has the proper implementations. - * The build keeps the .class coming from library/ and the .sjsir file from - * javalib/. This way, we bridge the library and javalib. But that means the - * binary interface of TypedArrayBufferBridge must be strictly equivalent in - * the two copies. - * - * Because of these copies, we must also explicitly use `Any` instead of all - * JS types in the method signatures. The IR cleaner would replace any JS type - * by `Any` in the javalib, so if we don't write them like that in the library - * as well, there will be mismatches. - * - * (Yes, this is a hack.) - * !!!!! - */ - -package scala.scalajs.js.typedarray - -import java.nio._ - -private[typedarray] object TypedArrayBufferBridge { - def wrapInt8Array(array: Any): ByteBuffer = - ByteBuffer.wrapInt8Array(array.asInstanceOf[Int8Array]) - - def wrapUint16Array(array: Any): CharBuffer = - CharBuffer.wrapUint16Array(array.asInstanceOf[Uint16Array]) - - def wrapInt16Array(array: Any): ShortBuffer = - ShortBuffer.wrapInt16Array(array.asInstanceOf[Int16Array]) - - def wrapInt32Array(array: Any): IntBuffer = - IntBuffer.wrapInt32Array(array.asInstanceOf[Int32Array]) - - def wrapFloat32Array(array: Any): FloatBuffer = - FloatBuffer.wrapFloat32Array(array.asInstanceOf[Float32Array]) - - def wrapFloat64Array(array: Any): DoubleBuffer = - DoubleBuffer.wrapFloat64Array(array.asInstanceOf[Float64Array]) - - def Buffer_hasArrayBuffer(buffer: Buffer): Boolean = - buffer.hasArrayBuffer() - - def Buffer_arrayBuffer(buffer: Buffer): Any = - buffer.arrayBuffer() - - def Buffer_arrayBufferOffset(buffer: Buffer): Int = - buffer.arrayBufferOffset() - - def Buffer_dataView(buffer: Buffer): Any = - buffer.dataView() - - def Buffer_hasTypedArray(buffer: Buffer): Boolean = - buffer.hasTypedArray() - - def Buffer_typedArray(buffer: Buffer): Any = - buffer.typedArray() -} diff --git a/javalibintf/src/main/java/org/scalajs/javalibintf/StackTraceElement.java b/javalibintf/src/main/java/org/scalajs/javalibintf/StackTraceElement.java new file mode 100644 index 0000000000..b2cdb29b62 --- /dev/null +++ b/javalibintf/src/main/java/org/scalajs/javalibintf/StackTraceElement.java @@ -0,0 +1,74 @@ +/* + * 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.javalibintf; + +/** + * Scala.js-specific extensions for {@link java.lang.StackTraceElement}. + * + *

In the JavaScript ecosystem, it is common practice for stack traces to + * mention column numbers in addition to line numbers. The official API of + * {@link java.lang.StackTraceElement} does not allow for representing column + * numbers, but Scala.js supports them. + * + *

This class offers methods to manipulate the extended information of + * {@link java.lang.StackTraceElement} for Scala.js. + * + *

This class only contains static methods. It cannot be instantiated. + * + * @see java.lang.StackTraceElement + */ +public final class StackTraceElement { + private StackTraceElement() {} + + /** + * Creates a {@link java.lang.StackTraceElement} that includes a column number. + * + * @param declaringClass + * the fully qualified name of the class containing the execution point + * represented by the stack trace element + * @param methodName + * the name of the method containing the execution point represented by the + * stack trace element + * @param fileName + * the name of the file containing the execution point represented by the + * stack trace element, or null if this information is unavailable + * @param lineNumber + * the line number of the source line containing the execution point + * represented by this stack trace element, or a negative number if this + * information is unavailable + * @param columnNumber + * the column number within the source line containing the execution point + * represented by this stack trace element, or a negative number if this + * information is unavailable + * + * @return + * a new {@link java.lang.StackTraceElement} containing the provided information + */ + public static final java.lang.StackTraceElement createWithColumnNumber( + String declaringClass, String methodName, String fileName, + int lineNumber, int columnNumber) { + throw new AssertionError("stub"); + } + + /** + * Returns the column number of the provided {@link java.lang.StackTraceElement}. + * + * @return + * the column number of the provided stackTraceElement, or a negative + * number if this information is unavailable + */ + public static final int getColumnNumber( + java.lang.StackTraceElement stackTraceElement) { + throw new AssertionError("stub"); + } +} diff --git a/javalibintf/src/main/java/org/scalajs/javalibintf/TypedArrayBuffer.java b/javalibintf/src/main/java/org/scalajs/javalibintf/TypedArrayBuffer.java new file mode 100644 index 0000000000..436b6d7fec --- /dev/null +++ b/javalibintf/src/main/java/org/scalajs/javalibintf/TypedArrayBuffer.java @@ -0,0 +1,315 @@ +/* + * 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.javalibintf; + +import java.nio.*; + +/** + * Utilities to interface {@link java.nio.Buffer}s and JavaScript TypedArrays. + * + *

{@link java.nio.Buffer}s can be direct buffers or + * indirect buffers. Indirect buffers use an underlying array (like + * {@code int[]} in Java or {@code Array[Int]} in Scala). Direct buffers are + * supposed to use off-heap memory. + * + *

In a JavaScript environment, the equivalent of off-heap memory for + * buffers of primitive numeric types are TypedArrays. + * + *

This class provides methods to wrap TypedArrays as direct Buffers, and + * extract references to TypedArrays from direct Buffers. + */ +public final class TypedArrayBuffer { + private TypedArrayBuffer() {} + + /** + * Wraps a JavaScript {@code Int8Array} as a direct + * {@link java.nio.ByteBuffer}. + * + *

The provided {@code array} parameter must be a valid JavaScript + * {@code Int8Array}, otherwise the behavior of this method is not + * specified. + * + *

The returned {@link java.nio.ByteBuffer} has the following properties: + * + *

    + *
  • It has a {@code capacity()} equal to the {@code array.length}.
  • + *
  • Its initial {@code position()} is 0 and its {@code limit()} is its capacity.
  • + *
  • It is a direct buffer backed by the provided {@code Int8Array}: + * changes to one are reflected on the other.
  • + *
+ * + * @param array a JavaScript {@code Int8Array} + */ + public static final ByteBuffer wrapInt8Array(Object array) { + throw new AssertionError("stub"); + } + + /** + * Wraps a JavaScript {@code Uint16Array} as a direct + * {@link java.nio.CharBuffer}. + * + *

The provided {@code array} parameter must be a valid JavaScript + * {@code Uint16Array}, otherwise the behavior of this method is not + * specified. + * + *

The returned {@link java.nio.CharBuffer} has the following properties: + * + *

    + *
  • It has a {@code capacity()} equal to the {@code array.length}.
  • + *
  • Its initial {@code position()} is 0 and its {@code limit()} is its capacity.
  • + *
  • It is a direct buffer backed by the provided {@code Uint16Array}: + * changes to one are reflected on the other.
  • + *
+ * + * @param array a JavaScript {@code Uint16Array} + */ + public static final CharBuffer wrapUint16Array(Object array) { + throw new AssertionError("stub"); + } + + /** + * Wraps a JavaScript {@code Int16Array} as a direct + * {@link java.nio.ShortBuffer}. + * + *

The provided {@code array} parameter must be a valid JavaScript + * {@code Int16Array}, otherwise the behavior of this method is not + * specified. + * + *

The returned {@link java.nio.ShortBuffer} has the following properties: + * + *

    + *
  • It has a {@code capacity()} equal to the {@code array.length}.
  • + *
  • Its initial {@code position()} is 0 and its {@code limit()} is its capacity.
  • + *
  • It is a direct buffer backed by the provided {@code Int16Array}: + * changes to one are reflected on the other.
  • + *
+ * + * @param array a JavaScript {@code Int16Array} + */ + public static final ShortBuffer wrapInt16Array(Object array) { + throw new AssertionError("stub"); + } + + /** + * Wraps a JavaScript {@code Int32Array} as a direct + * {@link java.nio.IntBuffer}. + * + *

The provided {@code array} parameter must be a valid JavaScript + * {@code Int32Array}, otherwise the behavior of this method is not + * specified. + * + *

The returned {@link java.nio.IntBuffer} has the following properties: + * + *

    + *
  • It has a {@code capacity()} equal to the {@code array.length}.
  • + *
  • Its initial {@code position()} is 0 and its {@code limit()} is its capacity.
  • + *
  • It is a direct buffer backed by the provided {@code Int32Array}: + * changes to one are reflected on the other.
  • + *
+ * + * @param array a JavaScript {@code Int32Array} + */ + public static final IntBuffer wrapInt32Array(Object array) { + throw new AssertionError("stub"); + } + + /** + * Wraps a JavaScript {@code Float32Array} as a direct + * {@link java.nio.FloatBuffer}. + * + *

The provided {@code array} parameter must be a valid JavaScript + * {@code Float32Array}, otherwise the behavior of this method is not + * specified. + * + *

The returned {@link java.nio.FloatBuffer} has the following properties: + * + *

    + *
  • It has a {@code capacity()} equal to the {@code array.length}.
  • + *
  • Its initial {@code position()} is 0 and its {@code limit()} is its capacity.
  • + *
  • It is a direct buffer backed by the provided {@code Float32Array}: + * changes to one are reflected on the other.
  • + *
+ * + * @param array a JavaScript {@code Float32Array} + */ + public static final FloatBuffer wrapFloat32Array(Object array) { + throw new AssertionError("stub"); + } + + /** + * Wraps a JavaScript {@code Float64Array} as a direct + * {@link java.nio.DoubleBuffer}. + * + *

The provided {@code array} parameter must be a valid JavaScript + * {@code Float64Array}, otherwise the behavior of this method is not + * specified. + * + *

The returned {@link java.nio.DoubleBuffer} has the following properties: + * + *

    + *
  • It has a {@code capacity()} equal to the {@code array.length}.
  • + *
  • Its initial {@code position()} is 0 and its {@code limit()} is its capacity.
  • + *
  • It is a direct buffer backed by the provided {@code Float64Array}: + * changes to one are reflected on the other.
  • + *
+ * + * @param array a JavaScript {@code Float64Array} + */ + public static final DoubleBuffer wrapFloat64Array(Object array) { + throw new AssertionError("stub"); + } + + /** + * Tests whether the given {@link java.nio.Buffer} is backed by an accessible + * JavaScript {@code ArrayBuffer}. + * + *

This is true for all read-write direct buffers, in particular for those + * created with any of the {@code wrapX} methods of this class. + * + *

If this method returns {@code true}, then {@code arrayBuffer(buffer)}, + * {@code arrayBufferOffset(buffer)} and {@code dataView(buffer)} do not + * throw any {@link UnsupportedOperationException}. + * + * @return + * true if and only if the provided {@code buffer} is backed by an + * accessible JavaScript {@code ArrayBuffer} + * + * @see TypedArrayBuffer#arrayBuffer(Buffer) + * @see TypedArrayBuffer#arrayBufferOffset(Buffer) + * @see TypedArrayBuffer#dataView(Buffer) + */ + public static final boolean hasArrayBuffer(Buffer buffer) { + throw new AssertionError("stub"); + } + + /** + * Returns the JavaScript {@code ArrayBuffer} backing the provided + * {@link java.nio.Buffer}. + * + *

The {@code buffer} may represent a view of the returned + * {@code ArrayBuffer} that does not start at index 0. Use the method + * {@link TypedArrayBuffer#arrayBufferOffset(Buffer)} to retrieve the offset + * within the {@code ArrayBuffer}. + * + * @return + * the JavaScript {@code ArrayBuffer} backing the provided {@code buffer} + * + * @throws UnsupportedOperationException + * if the provided {@code buffer} is read-only or is not backed by a + * JavaScript {@code ArrayBuffer}, i.e., if {@code hasArrayBuffer(buffer)} + * returns {@code false} + * + * @see TypedArrayBuffer#hasArrayBuffer(Buffer) + * @see TypedArrayBuffer#arrayBufferOffset(Buffer) + */ + public static final Object arrayBuffer(Buffer buffer) throws UnsupportedOperationException { + throw new AssertionError("stub"); + } + + /** + * Returns the offset within the JavaScript {@code ArrayBuffer} backing the + * provided {@link java.nio.Buffer}. + * + * @return + * the offset within the JavaScript {@code ArrayBuffer} backing the + * provided {@code buffer} where the latter starts + * + * @throws UnsupportedOperationException + * if the provided {@code buffer} is read-only or is not backed by a + * JavaScript {@code ArrayBuffer}, i.e., if {@code hasArrayBuffer(buffer)} + * returns {@code false} + * + * @see TypedArrayBuffer#hasArrayBuffer(Buffer) + * @see TypedArrayBuffer#arrayBuffer(Buffer) + */ + public static final int arrayBufferOffset(Buffer buffer) throws UnsupportedOperationException { + throw new AssertionError("stub"); + } + + /** + * Returns a JavaScript {@code DataView} of the provided + * {@link java.nio.Buffer}. + * + * @return + * a JavaScript {@code DataView} of the provided {@code buffer} + * + * @throws UnsupportedOperationException + * if the provided {@code buffer} is read-only or is not backed by a + * JavaScript {@code ArrayBuffer}, i.e., if {@code hasArrayBuffer(buffer)} + * returns {@code false} + * + * @see TypedArrayBuffer#hasArrayBuffer(Buffer) + */ + public static final Object dataView(Buffer buffer) throws UnsupportedOperationException { + throw new AssertionError("stub"); + } + + /** + * Tests whether the given {@link java.nio.Buffer} is backed by an accessible + * JavaScript {@code TypedArray}. + * + *

This is true when all of the following conditions apply: + * + *

    + *
  • the buffer is a direct buffer,
  • + *
  • it is not read-only,
  • + *
  • its byte order corresponds to the native byte order of JavaScript + * {@code TypedArray}s, and
  • + *
  • it is not a {@link java.nio.LongBuffer}.
  • + *
+ * + *

In particular, it is true for all {@link java.nio.Buffer}s created with + * any of the {@code wrapXArray} methods of this class. + * + *

If this method returns {@code true}, then {@code typedArray(buffer)} + * does not throw any {@link UnsupportedOperationException}. + * + * @return + * true if and only if the provided {@code buffer} is backed by an + * accessible JavaScript {@code TypedArray} + * + * @see TypedArrayBuffer#typedArray(Buffer) + */ + public static final boolean hasTypedArray(Buffer buffer) { + throw new AssertionError("stub"); + } + + /** + * Returns a JavaScript {@code TypedArray} view of the provided + * {@link java.nio.Buffer}. + * + *

The particular type of {@code TypedArray} depends on the type of buffer: + * + *

    + *
  • an {@code Int8Array} for a {@link java.nio.ByteBuffer}
  • + *
  • a {@code Uint16Array} for a {@link java.nio.CharBuffer}
  • + *
  • an {@code Int16Array} for a {@link java.nio.ShortBuffer}
  • + *
  • an {@code Int32Array} for a {@link java.nio.IntBuffer}
  • + *
  • an {@code Float32Array} for a {@link java.nio.FloatBuffer}
  • + *
  • an {@code Float64Array} for a {@link java.nio.DoubleBuffer}
  • + *
+ * + * @return + * a JavaScript {@code TypedArray} view of the provided {@code buffer} + * + * @throws UnsupportedOperationException + * if the provided {@code buffer} is read-only or is not backed by a + * JavaScript {@code TypedArray}, i.e., if {@code hasTypedArray(buffer)} + * returns {@code false} + * + * @see TypedArrayBuffer#hasTypedArray(Buffer) + */ + public static final Object typedArray(Buffer buffer) throws UnsupportedOperationException { + throw new AssertionError("stub"); + } +} diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala index b8d32b3b65..6b91311185 100644 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala +++ b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBuffer.scala @@ -16,6 +16,8 @@ import scala.language.implicitConversions import java.nio._ +import org.scalajs.javalibintf.{TypedArrayBuffer => Intf} + /** Factory methods to create direct buffers from Typed Arrays. * * All buffers created by the methods of this object are direct buffers with @@ -44,25 +46,25 @@ object TypedArrayBuffer { /** Wraps an [[Int8Array]] in a direct [[java.nio.ByteBuffer ByteBuffer]]. */ def wrap(array: Int8Array): ByteBuffer = - TypedArrayBufferBridge.wrapInt8Array(array) + Intf.wrapInt8Array(array) /** Wraps a [[Uint16Array]] in a direct [[java.nio.CharBuffer CharBuffer]]. */ def wrap(array: Uint16Array): CharBuffer = - TypedArrayBufferBridge.wrapUint16Array(array) + Intf.wrapUint16Array(array) /** Wraps an [[Int16Array]] in a direct [[java.nio.ShortBuffer ShortBuffer]]. */ def wrap(array: Int16Array): ShortBuffer = - TypedArrayBufferBridge.wrapInt16Array(array) + Intf.wrapInt16Array(array) /** Wraps an [[Int32Array]] in a direct [[java.nio.IntBuffer IntBuffer]]. */ def wrap(array: Int32Array): IntBuffer = - TypedArrayBufferBridge.wrapInt32Array(array) + Intf.wrapInt32Array(array) /** Wraps a [[Float32Array]] in a direct [[java.nio.FloatBuffer FloatBuffer]]. */ def wrap(array: Float32Array): FloatBuffer = - TypedArrayBufferBridge.wrapFloat32Array(array) + Intf.wrapFloat32Array(array) /** Wraps a [[Float64Array]] in a direct [[java.nio.DoubleBuffer DoubleBuffer]]. */ def wrap(array: Float64Array): DoubleBuffer = - TypedArrayBufferBridge.wrapFloat64Array(array) + Intf.wrapFloat64Array(array) } diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala deleted file mode 100644 index 3c0e57d400..0000000000 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferBridge.scala +++ /dev/null @@ -1,70 +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. - */ - -/* !!!!! - * THIS FILE IS ALMOST COPY-PASTED IN javalib/ AND library/. - * THEY MUST BE KEPT IN SYNC. - * - * This file acts as bridge for scala.scalajs.js.typedarray to be able to - * access the additional public API provided by java.nio, but which is not - * part of the JDK API. Because javalib/ does not export its .class files, - * we cannot call this additional API directly from library/, even though the - * members are public. - * - * In library/, this file has only the signatures, with stub implementations. - * In javalib/, it has the proper implementations. - * The build keeps the .class coming from library/ and the .sjsir file from - * javalib/. This way, we bridge the library and javalib. But that means the - * binary interface of TypedArrayBufferBridge must be strictly equivalent in - * the two copies. - * - * Because of these copies, we must also explicitly use `Any` instead of all - * JS types in the method signatures. The IR cleaner would replace any JS type - * by `Any` in the javalib, so if we don't write them like that in the library - * as well, there will be mismatches. - * - * (Yes, this is a hack.) - * !!!!! - */ - -package scala.scalajs.js.typedarray - -import java.nio._ - -private[typedarray] object TypedArrayBufferBridge { - def wrapInt8Array(array: Any): ByteBuffer = stub() - - def wrapUint16Array(array: Any): CharBuffer = stub() - - def wrapInt16Array(array: Any): ShortBuffer = stub() - - def wrapInt32Array(array: Any): IntBuffer = stub() - - def wrapFloat32Array(array: Any): FloatBuffer = stub() - - def wrapFloat64Array(array: Any): DoubleBuffer = stub() - - def Buffer_hasArrayBuffer(buffer: Buffer): Boolean = stub() - - def Buffer_arrayBuffer(buffer: Buffer): Any = stub() - - def Buffer_arrayBufferOffset(buffer: Buffer): Int = stub() - - def Buffer_dataView(buffer: Buffer): Any = stub() - - def Buffer_hasTypedArray(buffer: Buffer): Boolean = stub() - - def Buffer_typedArray(buffer: Buffer): Any = stub() - - private def stub(): Nothing = - throw new Error("stub") -} diff --git a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala index b0c1911a0c..f24a2f2227 100644 --- a/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala +++ b/library/src/main/scala/scala/scalajs/js/typedarray/TypedArrayBufferOps.scala @@ -16,6 +16,8 @@ import scala.language.implicitConversions import java.nio._ +import org.scalajs.javalibintf.{TypedArrayBuffer => Intf} + /** Additional operations on a [[java.nio.Buffer Buffer]] with interoperability * with JavaScript Typed Arrays. * @@ -33,7 +35,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * This is true iff the buffer is direct and not read-only. */ def hasArrayBuffer(): Boolean = - TypedArrayBufferBridge.Buffer_hasArrayBuffer(buffer) + Intf.hasArrayBuffer(buffer) /** [[ArrayBuffer]] backing this buffer _(optional operation)_. * @@ -41,7 +43,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * If this buffer has no backing [[ArrayBuffer]], i.e., !hasArrayBuffer() */ def arrayBuffer(): ArrayBuffer = - TypedArrayBufferBridge.Buffer_arrayBuffer(buffer).asInstanceOf[ArrayBuffer] + Intf.arrayBuffer(buffer).asInstanceOf[ArrayBuffer] /** Byte offset in the associated [[ArrayBuffer]] _(optional operation)_. * @@ -49,7 +51,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * If this buffer has no backing [[ArrayBuffer]], i.e., !hasArrayBuffer() */ def arrayBufferOffset(): Int = - TypedArrayBufferBridge.Buffer_arrayBufferOffset(buffer) + Intf.arrayBufferOffset(buffer) /** [[DataView]] of the backing [[ArrayBuffer]] _(optional operation)_. * @@ -60,7 +62,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * If this buffer has no backing [[ArrayBuffer]], i.e., !hasArrayBuffer() */ def dataView(): DataView = - TypedArrayBufferBridge.Buffer_dataView(buffer).asInstanceOf[DataView] + Intf.dataView(buffer).asInstanceOf[DataView] /** Tests whether this direct buffer has a valid associated [[TypedArray]]. * @@ -74,7 +76,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * only if their byte order is the native order of the platform. */ def hasTypedArray(): Boolean = - TypedArrayBufferBridge.Buffer_hasTypedArray(buffer) + Intf.hasTypedArray(buffer) /** [[TypedArray]] backing this direct buffer _(optional operation)_. * @@ -85,7 +87,7 @@ final class TypedArrayBufferOps[ // scalastyle:ignore * If this buffer does not have a backing [[TypedArray]], i.e., !hasTypedArray(). */ def typedArray(): TypedArrayType = - TypedArrayBufferBridge.Buffer_typedArray(buffer).asInstanceOf[TypedArrayType] + Intf.typedArray(buffer).asInstanceOf[TypedArrayType] } /** Extensions to [[java.nio.Buffer Buffer]]s for interoperability with diff --git a/project/Build.scala b/project/Build.scala index 6f2131e2cc..cf910caa31 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -713,6 +713,7 @@ object Build { compiler, irProject, irProjectJS, linkerInterface, linkerInterfaceJS, linker, linkerJS, testAdapter, + javalibintf, javalib, scalalib, libraryAux, library, testInterface, jUnitRuntime, testBridge, jUnitPlugin, jUnitAsyncJS, jUnitAsyncJVM, jUnitTestOutputsJS, jUnitTestOutputsJVM, @@ -1195,6 +1196,17 @@ object Build { } } + lazy val javalibintf: Project = Project( + id = "javalibintf", base = file("javalibintf") + ).settings( + commonSettings, + publishSettings(Some(VersionScheme.BreakOnMajor)), + name := "scalajs-javalib-intf", + + crossPaths := false, + autoScalaLibrary := false, + ) + lazy val javalib: MultiScalaProject = MultiScalaProject( id = "javalib", base = file("javalib") ).enablePlugins( @@ -1427,6 +1439,8 @@ object Build { id = "library", base = file("library") ).enablePlugins( MyScalaJSPlugin + ).dependsOn( + javalibintf % Provided, ).settings( commonSettings, publishSettings(Some(VersionScheme.BreakOnMajor)), @@ -1506,15 +1520,7 @@ object Build { * (but not .class files) */ mappings in packageBin := { - /* From library, we must take everything, except the - * java.nio.TypedArrayBufferBridge object, whose actual - * implementation is in javalib. - */ - val superMappings = (mappings in packageBin).value - val libraryMappings = superMappings.filter { mapping => - !mapping._2.replace('\\', '/').startsWith( - "scala/scalajs/js/typedarray/TypedArrayBufferBridge") - } + val libraryMappings = (mappings in packageBin).value val filter = ("*.sjsir": NameFilter) diff --git a/project/MultiScalaProject.scala b/project/MultiScalaProject.scala index 12f917da30..b4e7ce3ac8 100644 --- a/project/MultiScalaProject.scala +++ b/project/MultiScalaProject.scala @@ -27,6 +27,9 @@ final class MultiScalaProject private (private val projects: Map[String, Project zipped(depsByVersion)(_.dependsOn(_: _*)) } + def dependsOn(deps: ClasspathDependency*)(implicit dummy: DummyImplicit): MultiScalaProject = + transform(_.dependsOn(deps: _*)) + def configs(cs: Configuration*): MultiScalaProject = transform(_.configs(cs: _*)) diff --git a/scripts/publish.sh b/scripts/publish.sh index 2234872f53..4c7aee070a 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -9,10 +9,18 @@ fi SUFFIXES="2_11 2_12 2_13" +JAVA_LIBS="javalibintf" COMPILER="compiler jUnitPlugin" JS_LIBS="library irJS linkerInterfaceJS linkerJS testInterface testBridge jUnitRuntime" JVM_LIBS="ir linkerInterface linker testAdapter" -LIBS="$JS_LIBS $JVM_LIBS" +SCALA_LIBS="$JS_LIBS $JVM_LIBS" + +# Publish Java libraries +ARGS="" +for p in $JAVA_LIBS; do + ARGS="$ARGS $p/publishSigned" +done +$CMD $ARGS # Publish compiler for s in $SUFFIXES; do @@ -23,10 +31,10 @@ for s in $SUFFIXES; do $CMD $ARGS done -# Publish libraries +# Publish Scala libraries for s in $SUFFIXES; do ARGS="" - for p in $LIBS; do + for p in $SCALA_LIBS; do ARGS="$ARGS $p$s/publishSigned" done $CMD $ARGS From 55ce893400e5bb0c498f5a453942235ebd703432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 3 Sep 2022 19:29:54 +0200 Subject: [PATCH 60/75] Check that classes have kind HijackedClass iff they are a hijacked class. --- .../scalajs/linker/checker/ClassDefChecker.scala | 11 ++++++++++- .../scala/org/scalajs/linker/AnalyzerTest.scala | 12 +++++++----- .../linker/checker/ClassDefCheckerTest.scala | 14 +++++++++++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index f6e4860014..275dadaf05 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -100,8 +100,17 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) } private def checkKind()(implicit ctx: ErrorContext): Unit = { - if (isJLObject && classDef.kind != ClassKind.Class) + val className = classDef.name.name + + if (isJLObject && classDef.kind != ClassKind.Class) { reportError("java.lang.Object must be a Class") + } else { + val isHijacked = HijackedClasses.contains(className) + if (isHijacked && classDef.kind != ClassKind.HijackedClass) + reportError(i"$className must be a HijackedClass") + else if (!isHijacked && classDef.kind == ClassKind.HijackedClass) + reportError(i"$className must not be a HijackedClass") + } } private def checkJSClassCaptures()(implicit ctx: ErrorContext): Unit = { diff --git a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala index 79736326b7..0573694839 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala @@ -128,7 +128,6 @@ class AnalyzerTest { val kindsSub = Seq( ClassKind.Class, ClassKind.ModuleClass, - ClassKind.HijackedClass, ClassKind.JSClass, ClassKind.JSModuleClass, ClassKind.NativeJSClass, @@ -139,7 +138,7 @@ class AnalyzerTest { def kindsBaseFor(kindSub: ClassKind): Seq[ClassKind] = { import ClassKind._ kindSub match { - case Class | ModuleClass | HijackedClass => + case Class | ModuleClass => Seq(Interface, ModuleClass, JSClass, NativeJSClass) case Interface => // interfaces are checked in the ClassDefChecker. @@ -147,6 +146,8 @@ class AnalyzerTest { case JSClass | JSModuleClass | NativeJSClass | NativeJSModuleClass | AbstractJSType => Seq(Class, Interface, AbstractJSType, JSModuleClass) + case HijackedClass => + throw new AssertionError("Cannot test HijackedClass because it fails earlier") } } @@ -179,7 +180,6 @@ class AnalyzerTest { val kindsCls = Seq( ClassKind.Class, ClassKind.ModuleClass, - ClassKind.HijackedClass, ClassKind.Interface, ClassKind.JSClass, ClassKind.JSModuleClass, @@ -191,12 +191,14 @@ class AnalyzerTest { def kindsIntfFor(kindCls: ClassKind): Seq[ClassKind] = { import ClassKind._ kindCls match { - case Class | ModuleClass | HijackedClass | Interface => + case Class | ModuleClass | Interface => Seq(Class, ModuleClass, JSClass, NativeJSClass, AbstractJSType) case JSClass | JSModuleClass | NativeJSClass | NativeJSModuleClass | AbstractJSType => - Seq(Class, ModuleClass, HijackedClass, Interface, JSClass, + Seq(Class, ModuleClass, Interface, JSClass, JSModuleClass, NativeJSClass, NativeJSModuleClass) + case HijackedClass => + throw new AssertionError("Cannot test HijackedClass because it fails earlier") } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala index 73b7339498..a5c34f30f4 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala @@ -47,6 +47,17 @@ class ClassDefCheckerTest { "java.lang.Object may not implement any interfaces") } + @Test + def hijackedClassesKinds(): Unit = { + assertError( + classDef(BoxedIntegerClass, kind = ClassKind.Class, superClass = Some(ObjectClass)), + "java.lang.Integer must be a HijackedClass") + + assertError( + classDef("A", kind = ClassKind.HijackedClass, superClass = Some(ObjectClass)), + "A must not be a HijackedClass") + } + @Test def missingSuperClass(): Unit = { val kinds = Seq( @@ -60,8 +71,9 @@ class ClassDefCheckerTest { ) for (kind <- kinds) { + val name = if (kind == ClassKind.HijackedClass) BoxedIntegerClass else ClassName("A") assertError( - classDef("A", kind = kind, memberDefs = requiredMemberDefs("A", kind)), + classDef(name, kind = kind, memberDefs = requiredMemberDefs(name, kind)), "missing superClass") } } From f0205cdba665d5cc28679291c7aa5de949435561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Sep 2022 17:15:46 +0200 Subject: [PATCH 61/75] Force the `tpe` of `This` nodes to have the most precise type. Previously, for `This` nodes, we checked that their `tpe` was a *subtype* of the expected this type. Now, we force it to always be the most precise, i.e., the exact this type for the enclosing class. This allows to move that check from the IR checker up to the ClassDef checker, as we do not need the subtyping relationships. It also allows to remove a hack in the FunctionEmitter where we had to compute the exact type for `This` nodes for correct boxing and unboxing of `char` values. At a fundamental level, this removes the only occurrence of *subsumption* for terms in the type system of our IR. Every term must have the most precise type it can have (but can be *used* in contexts that demand a supertype, obviously). We introduce a deserialization hack to patch up the `tpe` of `This` nodes coming from earlier versions of the IR, as the precise type was not enforced (and was indeed not always as precise as possible). In this commit, we apply the hack regardless of the IR version, and do not update the compiler yet, for testing purposes. --- .../scala/org/scalajs/ir/Serializers.scala | 42 ++++++++++-- .../backend/emitter/FunctionEmitter.scala | 19 +----- .../linker/checker/ClassDefChecker.scala | 47 ++++++++++--- .../scalajs/linker/checker/IRChecker.scala | 66 +++++-------------- .../org/scalajs/linker/AnalyzerTest.scala | 2 +- .../linker/checker/ClassDefCheckerTest.scala | 56 ++++++++++++++++ 6 files changed, 150 insertions(+), 82 deletions(-) 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 01897cf691..bc4fcc9306 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -1000,6 +1000,8 @@ object Serializers { private[this] var lastPosition: Position = Position.NoPosition + private[this] var thisTypeForHack8: Type = NoType + def deserializeEntryPointsInfo(): EntryPointsInfo = { hacks = new Hacks(sourceVersion = readHeader()) readEntryPointsInfo() @@ -1224,13 +1226,32 @@ object Serializers { case TagVarRef => VarRef(readLocalIdent())(readType()) + case TagThis => - This()(readType()) + val tpe = readType() + if (/*hacks.use8*/ true) // scalastyle:ignore + This()(thisTypeForHack8) + else + This()(tpe) + case TagClosure => val arrow = readBoolean() val captureParams = readParamDefs() val (params, restParam) = readParamDefsWithRest() - Closure(arrow, captureParams, params, restParam, readTree(), readTrees()) + val body = if (/*!hacks.use8*/ false) { // scalastyle:ignore + readTree() + } else { + val prevThisTypeForHack8 = thisTypeForHack8 + thisTypeForHack8 = if (arrow) NoType else AnyType + try { + readTree() + } finally { + thisTypeForHack8 = prevThisTypeForHack8 + } + } + val captureValues = readTrees() + Closure(arrow, captureParams, params, restParam, body, captureValues) + case TagCreateJSClass => CreateJSClass(readClassName(), readTrees()) } @@ -1319,8 +1340,21 @@ object Serializers { def readClassDef(): ClassDef = { implicit val pos = readPosition() val name = readClassIdent() + val cls = name.name val originalName = readOriginalName() val kind = ClassKind.fromByte(readByte()) + + if (/*hacks.use8*/ true) { // scalastyle:ignore + thisTypeForHack8 = { + if (kind.isJSType) + AnyType + else if (kind == ClassKind.HijackedClass) + BoxedClassToPrimType.getOrElse(cls, ClassType(cls)) // getOrElse as safety guard + else + ClassType(cls) + } + } + val hasJSClassCaptures = readBoolean() val jsClassCaptures = if (!hasJSClassCaptures) None @@ -1335,8 +1369,8 @@ object Serializers { val jsSuperClass = readOptTree() val jsNativeLoadSpec = readJSNativeLoadSpec() - val memberDefs0 = readMemberDefs(name.name, kind) - val topLevelExportDefs = readTopLevelExportDefs(name.name, kind) + val memberDefs0 = readMemberDefs(cls, kind) + val topLevelExportDefs = readTopLevelExportDefs(cls, kind) val optimizerHints = OptimizerHints.fromBits(readInt()) val memberDefs = diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index 4d40011643..87b99168ad 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -2229,7 +2229,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { * not already a CharType, we must introduce a cast to unbox the * value. */ - if (realTreeType(receiver) == CharType) + if (receiver.tpe == CharType) transformExpr(receiver, preserveChar = true) else transformExpr(AsInstanceOf(receiver, CharType), preserveChar = true) @@ -2981,27 +2981,12 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { tree.getClass) } - if (preserveChar || realTreeType(tree) != CharType) + if (preserveChar || tree.tpe != CharType) baseResult else genCallHelper("bC", baseResult) } - private def realTreeType(tree: Tree)(implicit env: Env): Type = { - val tpe = tree.tpe - tree match { - case This() if tpe.isInstanceOf[ClassType] => - env.enclosingClassName match { - case Some(enclosingClassName) => - BoxedClassToPrimType.getOrElse(enclosingClassName, ClassType(enclosingClassName)) - case None => - tpe - } - case _ => - tpe - } - } - private def transformApplyDynamicImport(tree: ApplyDynamicImport)( implicit env: Env): js.Tree = { implicit val pos = tree.pos diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index 275dadaf05..3ebfd5f8e1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -33,6 +33,16 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) private[this] val isJLObject = classDef.name.name == ObjectClass + private[this] val instanceThisType: Type = { + val cls = classDef.name.name + if (classDef.kind.isJSType) + AnyType + else if (classDef.kind == ClassKind.HijackedClass) + BoxedClassToPrimType.getOrElse(cls, ClassType(cls)) // getOrElse not to crash on invalid ClassDef + else + ClassType(cls) + } + private[this] val fields = Array.fill(MemberNamespace.Count)(mutable.Map.empty[FieldName, Type]) @@ -266,7 +276,8 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) } // Body - body.foreach(checkTree(_, Env.fromParams(params))) + val thisType = if (static) NoType else instanceThisType + body.foreach(checkTree(_, Env.fromParams(params).withThisType(thisType))) } private def checkJSConstructorDef(ctorDef: JSConstructorDef): Unit = withPerMethodState { @@ -290,7 +301,8 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) checkTree(stat, prevEnv) } checkTreeOrSpreads(body.superCall.args, envJustBeforeSuper) - body.afterSuper.foldLeft(envJustBeforeSuper) { (prevEnv, stat) => + val envJustAfterSuper = envJustBeforeSuper.withThisType(instanceThisType) + body.afterSuper.foldLeft(envJustAfterSuper) { (prevEnv, stat) => checkTree(stat, prevEnv) } } @@ -316,8 +328,9 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) checkExportedPropertyName(pName) checkJSParamDefs(params, restParam) + val thisType = if (static) NoType else instanceThisType val env = - Env.fromParams(classDef.jsClassCaptures.getOrElse(Nil) ++ params ++ restParam) + Env.fromParams(classDef.jsClassCaptures.getOrElse(Nil) ++ params ++ restParam).withThisType(thisType) checkTree(body, env) } @@ -340,16 +353,21 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) checkExportedPropertyName(pName) + val jsClassCaptures = classDef.jsClassCaptures.getOrElse(Nil) + val thisType = if (static) NoType else instanceThisType + getterBody.foreach { body => withPerMethodState { - checkTree(body, Env.fromParams(classDef.jsClassCaptures.getOrElse(Nil))) + val bodyEnv = Env.fromParams(jsClassCaptures).withThisType(thisType) + checkTree(body, bodyEnv) } } setterArgAndBody.foreach { case (setterArg, body) => withPerMethodState { checkJSParamDefs(setterArg :: Nil, None) - checkTree(body, Env.fromParams(classDef.jsClassCaptures.getOrElse(Nil) :+ setterArg)) + val bodyEnv = Env.fromParams(jsClassCaptures :+ setterArg).withThisType(thisType) + checkTree(body, bodyEnv) } } } @@ -764,7 +782,10 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) } case This() => - // TODO: Force type to be exact? This would allow to remove thisType tracking from IRChecker. + if (env.thisType == NoType) + reportError(i"Cannot find `this` in scope") + else if (tree.tpe != env.thisType) + reportError(i"`this` of type ${env.thisType} typed as ${tree.tpe}") case Closure(arrow, captureParams, params, restParam, body, captureValues) => /* Check compliance of captureValues wrt. captureParams in the current @@ -793,6 +814,7 @@ private final class ClassDefChecker(classDef: ClassDef, reporter: ErrorReporter) val bodyEnv = Env .fromParams(captureParams ++ params ++ restParam) .withHasNewTarget(!arrow) + .withThisType(if (arrow) NoType else AnyType) checkTree(body, bodyEnv) } @@ -862,6 +884,8 @@ object ClassDefChecker { private class Env( /** Whether there is a valid `new.target` in scope. */ val hasNewTarget: Boolean, + /** The type of `this` in scope, or `NoType` if there is no `this` in scope. */ + val thisType: Type, /** Local variables in scope (including through closures). */ val locals: Map[LocalName, LocalDef], /** Return types by label. */ @@ -872,6 +896,9 @@ object ClassDefChecker { def withHasNewTarget(hasNewTarget: Boolean): Env = copy(hasNewTarget = hasNewTarget) + def withThisType(thisType: Type): Env = + copy(thisType = thisType) + def withLocal(localDef: LocalDef): Env = copy(locals = locals + (localDef.name -> localDef)) @@ -880,21 +907,23 @@ object ClassDefChecker { private def copy( hasNewTarget: Boolean = hasNewTarget, + thisType: Type = thisType, locals: Map[LocalName, LocalDef] = locals, returnLabels: Set[LabelName] = returnLabels ): Env = { - new Env(hasNewTarget, locals, returnLabels) + new Env(hasNewTarget, thisType, locals, returnLabels) } } private object Env { - val empty: Env = new Env(hasNewTarget = false, Map.empty, Set.empty) + val empty: Env = + new Env(hasNewTarget = false, thisType = NoType, Map.empty, Set.empty) def fromParams(params: List[ParamDef]): Env = { val paramLocalDefs = for (p @ ParamDef(ident, _, tpe, mutable) <- params) yield ident.name -> LocalDef(ident.name, tpe, mutable) - new Env(hasNewTarget = false, paramLocalDefs.toMap, Set.empty) + new Env(hasNewTarget = false, thisType = NoType, paramLocalDefs.toMap, Set.empty) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 90a160e518..e2a1f8a88f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -126,15 +126,9 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { i"The abstract method ${classDef.name.name}.$name survived the " + "Analyzer (this is a bug)") } { body => - val thisType = - if (static) NoType - else thisTypeForScalaClass(classDef) - - val inConstructorOf = - if (flags.namespace.isConstructor) Some(classDef.name.name) - else None - - val bodyEnv = Env.fromSignature(thisType, inConstructorOf) + val bodyEnv = + if (flags.namespace.isConstructor) Env.forConstructorOf(classDef.name.name) + else Env.empty typecheckExpect(body, bodyEnv, resultType) } @@ -147,12 +141,10 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { // JS constructors only get a valid `this` after the super call. - val beforeSuperEnv = Env.fromSignature(thisType = NoType, inConstructorOf = Some(clazz.name.name)) - body.beforeSuper.foreach(typecheck(_, beforeSuperEnv)) - body.superCall.args.foreach(typecheckExprOrSpread(_, beforeSuperEnv)) - - val afterSuperEnv = beforeSuperEnv.withThis(AnyType) - body.afterSuper.foreach(typecheck(_, afterSuperEnv)) + val bodyEnv = Env.forConstructorOf(clazz.name.name) + body.beforeSuper.foreach(typecheck(_, bodyEnv)) + body.superCall.args.foreach(typecheckExprOrSpread(_, bodyEnv)) + body.afterSuper.foreach(typecheck(_, bodyEnv)) val resultType = body.afterSuper.lastOption.fold[Type](NoType)(_.tpe) if (resultType == NoType) @@ -168,14 +160,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { typecheckExpr(pName, Env.empty) - val thisType = { - if (static) NoType - else if (clazz.kind.isJSClass) AnyType - else thisTypeForScalaClass(clazz) - } - - val bodyEnv = Env.fromSignature(thisType) - typecheckExpect(body, bodyEnv, AnyType) + typecheckExpect(body, Env.empty, AnyType) } private def checkJSPropertyDef(propDef: JSPropertyDef, @@ -185,24 +170,13 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { typecheckExpr(pName, Env.empty) - val thisType = - if (flags.namespace.isStatic) NoType - else if (clazz.kind.isJSClass) AnyType - else thisTypeForScalaClass(clazz) - - val env = Env.fromSignature(thisType) - - getterBody.foreach(typecheckExpr(_, env)) + getterBody.foreach(typecheckExpr(_, Env.empty)) setterArgAndBody.foreach { case (_, body) => - typecheck(body, env) + typecheck(body, Env.empty) } } - private def thisTypeForScalaClass(clazz: LinkedClass): Type = - if (clazz.kind == ClassKind.HijackedClass) BoxedClassToPrimType(clazz.name.name) - else ClassType(clazz.name.name) - private def typecheckExpect(tree: Tree, env: Env, expectedType: Type)( implicit ctx: ErrorContext): Unit = { typecheck(tree, env) @@ -690,8 +664,6 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { case _: VarRef => case This() => - if (!isSubtype(env.thisTpe, tree.tpe)) - reportError(i"this of type ${env.thisTpe} typed as ${tree.tpe}") case Closure(arrow, captureParams, params, restParam, body, captureValues) => assert(captureParams.size == captureValues.size) // checked by ClassDefChecker @@ -702,9 +674,7 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { } // Then check the closure params and body in its own env - val thisType = if (arrow) NoType else AnyType - val bodyEnv = Env.fromSignature(thisType) - typecheckExpect(body, bodyEnv, AnyType) + typecheckExpect(body, Env.empty, AnyType) case CreateJSClass(className, captureValues) => val clazz = lookupClass(className) @@ -813,8 +783,6 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { } private class Env( - /** Type of `this`. Can be NoType. */ - val thisTpe: Type, /** Return types by label. */ val returnTypes: Map[LabelName, Type], /** Whether we're in a constructor of the class */ @@ -822,19 +790,15 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { ) { import Env._ - def withThis(thisTpe: Type): Env = - new Env(thisTpe, this.returnTypes, this.inConstructorOf) - def withLabeledReturnType(label: LabelName, returnType: Type): Env = - new Env(this.thisTpe, returnTypes + (label -> returnType), this.inConstructorOf) + new Env(returnTypes + (label -> returnType), this.inConstructorOf) } private object Env { - val empty: Env = new Env(NoType, Map.empty, None) + val empty: Env = new Env(Map.empty, None) - def fromSignature(thisType: Type, inConstructorOf: Option[ClassName] = None): Env = { - new Env(thisType, Map.empty, inConstructorOf) - } + def forConstructorOf(className: ClassName): Env = + new Env(Map.empty, inConstructorOf = Some(className)) } private class CheckedClass( diff --git a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala index 0573694839..0f05ba1c79 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala @@ -211,7 +211,7 @@ class AnalyzerTest { memberDefs = requiredMemberDefs("A", kindCls)), classDef("B", kind = kindIntf, superClass = validParentForKind(kindIntf), - memberDefs = requiredMemberDefs("A", kindIntf)) + memberDefs = requiredMemberDefs("B", kindIntf)) ) val analysis = computeAnalysis(classDefs, diff --git a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala index a5c34f30f4..ab357f2455 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala @@ -180,6 +180,62 @@ class ClassDefCheckerTest { "Duplicate local variable name x." ) } + + @Test + def thisType(): Unit = { + def testThisTypeError(static: Boolean, expr: Tree, expectedMsg: String): Unit = { + val methodFlags = + if (static) EMF.withNamespace(MemberNamespace.PublicStatic) + else EMF + + assertError( + classDef( + "Foo", superClass = Some(ObjectClass), + memberDefs = List( + MethodDef(methodFlags, m("bar", Nil, V), NON, Nil, NoType, Some({ + consoleLog(expr) + }))(EOH, None) + ) + ), + expectedMsg) + } + + testThisTypeError(static = true, + This()(NoType), + "Cannot find `this` in scope") + + testThisTypeError(static = true, + This()(ClassType("Foo")), + "Cannot find `this` in scope") + + testThisTypeError(static = false, + This()(NoType), + "`this` of type Foo typed as ") + + testThisTypeError(static = false, + This()(AnyType), + "`this` of type Foo typed as any") + + testThisTypeError(static = false, + This()(ClassType("Bar")), + "`this` of type Foo typed as Bar") + + testThisTypeError(static = false, + Closure(arrow = true, Nil, Nil, None, This()(NoType), Nil), + "Cannot find `this` in scope") + + testThisTypeError(static = false, + Closure(arrow = true, Nil, Nil, None, This()(AnyType), Nil), + "Cannot find `this` in scope") + + testThisTypeError(static = false, + Closure(arrow = false, Nil, Nil, None, This()(NoType), Nil), + "`this` of type any typed as ") + + testThisTypeError(static = false, + Closure(arrow = false, Nil, Nil, None, This()(ClassType("Foo")), Nil), + "`this` of type any typed as Foo") + } } private object ClassDefCheckerTest { From 3fb2b965472dbb0ecb0dfa7533138f2c0a2cba2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Sep 2022 17:34:26 +0200 Subject: [PATCH 62/75] Emit This nodes with the most precise type in the compiler back-end. As followup to the parent commit, we update the compiler back-end to always emit `This` nodes with their most precise type. We condition the deserialization hack to reading IR from earlier versions. --- .../src/main/scala/org/scalajs/nscplugin/GenJSCode.scala | 2 +- .../main/scala/org/scalajs/nscplugin/GenJSExports.scala | 8 +++----- ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala | 6 +++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 06e9572fe6..6925027717 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -151,7 +151,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private val fieldsMutatedInCurrentClass = new ScopedVar[mutable.Set[Name]] private val generatedSAMWrapperCount = new ScopedVar[VarBox[Int]] - private def currentThisType: jstpe.Type = { + def currentThisType: jstpe.Type = { encodeClassType(currentClassSym) match { case tpe @ jstpe.ClassType(cls) => jstpe.BoxedClassToPrimType.getOrElse(cls, tpe) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala index 994f8d95ce..4239c9c11a 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala @@ -624,7 +624,7 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { } } - val receiver = js.This()(jstpe.AnyType) + val receiver = js.This()(currentThisType) val nameString = genExpr(jsNameOf(sym)) if (jsInterop.isJSGetter(sym)) { @@ -708,7 +708,7 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { * dispatchers, we know the default getter is on `this`. This applies * to both top-level and nested classes. */ - (owner, js.This()(encodeClassType(owner))) + (owner, js.This()(currentThisType)) } else if (isNested) { assert(captures.size == 1, s"expected exactly one capture got $captures ($sym at $pos)") @@ -816,10 +816,8 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { def receiver = { if (static) genLoadModule(sym.owner) - else if (sym.owner == ObjectClass) - js.This()(jstpe.ClassType(ir.Names.ObjectClass)) else - js.This()(encodeClassType(sym.owner)) + js.This()(currentThisType) } if (isNonNativeJSClass(currentClassSym)) { 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 bc4fcc9306..43c00f988a 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -1229,7 +1229,7 @@ object Serializers { case TagThis => val tpe = readType() - if (/*hacks.use8*/ true) // scalastyle:ignore + if (hacks.use8) This()(thisTypeForHack8) else This()(tpe) @@ -1238,7 +1238,7 @@ object Serializers { val arrow = readBoolean() val captureParams = readParamDefs() val (params, restParam) = readParamDefsWithRest() - val body = if (/*!hacks.use8*/ false) { // scalastyle:ignore + val body = if (!hacks.use8) { readTree() } else { val prevThisTypeForHack8 = thisTypeForHack8 @@ -1344,7 +1344,7 @@ object Serializers { val originalName = readOriginalName() val kind = ClassKind.fromByte(readByte()) - if (/*hacks.use8*/ true) { // scalastyle:ignore + if (hacks.use8) { thisTypeForHack8 = { if (kind.isJSType) AnyType From 7d0802ca2a0e8b03a0c6a98b35fd9ece5384bc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 6 Sep 2022 11:15:43 +0200 Subject: [PATCH 63/75] Upgrade to scalajs-js-envs 1.4.0. --- project/Build.scala | 12 ++++++------ project/build.sbt | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index cf910caa31..6978335c0b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1011,8 +1011,8 @@ object Build { libraryDependencies ++= Seq( "com.google.javascript" % "closure-compiler" % "v20220202", "com.google.jimfs" % "jimfs" % "1.1" % "test", - "org.scala-js" %% "scalajs-env-nodejs" % "1.3.0" % "test", - "org.scala-js" %% "scalajs-js-envs-test-kit" % "1.3.0" % "test" + "org.scala-js" %% "scalajs-env-nodejs" % "1.4.0" % "test", + "org.scala-js" %% "scalajs-js-envs-test-kit" % "1.4.0" % "test" ) ++ ( parallelCollectionsDependencies(scalaVersion.value) ), @@ -1084,7 +1084,7 @@ object Build { name := "Scala.js sbt test adapter", libraryDependencies ++= Seq( "org.scala-sbt" % "test-interface" % "1.0", - "org.scala-js" %% "scalajs-js-envs" % "1.3.0", + "org.scala-js" %% "scalajs-js-envs" % "1.4.0", "com.google.jimfs" % "jimfs" % "1.1" % "test", ), libraryDependencies ++= JUnitDeps, @@ -1113,8 +1113,8 @@ object Build { mimaBinaryIssueFilters ++= BinaryIncompatibilities.SbtPlugin, addSbtPlugin("org.portable-scala" % "sbt-platform-deps" % "1.0.1"), - libraryDependencies += "org.scala-js" %% "scalajs-js-envs" % "1.3.0", - libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.3.0", + libraryDependencies += "org.scala-js" %% "scalajs-js-envs" % "1.4.0", + libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.4.0", scriptedLaunchOpts += "-Dplugin.version=" + version.value, @@ -2270,7 +2270,7 @@ object Build { resolvers += Resolver.typesafeIvyRepo("releases"), - libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.3.0", + libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.4.0", artifactPath in fetchScalaSource := baseDirectory.value.getParentFile / "fetchedSources" / scalaVersion.value, diff --git a/project/build.sbt b/project/build.sbt index ad22f838a4..86b3eaa21f 100644 --- a/project/build.sbt +++ b/project/build.sbt @@ -12,8 +12,8 @@ libraryDependencies += "com.google.jimfs" % "jimfs" % "1.1" libraryDependencies += "org.eclipse.jgit" % "org.eclipse.jgit.pgm" % "3.2.0.201312181205-r" -libraryDependencies += "org.scala-js" %% "scalajs-js-envs" % "1.3.0" -libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.3.0" +libraryDependencies += "org.scala-js" %% "scalajs-js-envs" % "1.4.0" +libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.4.0" Compile / unmanagedSourceDirectories ++= { val root = baseDirectory.value.getParentFile From 5b5525763b0fd119965ee9bb39d2f7cc52fb6270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 6 Sep 2022 11:18:13 +0200 Subject: [PATCH 64/75] Fix #4686: Take the `envVars` sbt setting into account. For `run`, it is read in the scope `project/Config/run`. For test tasks, it is read in the scope `project/Test` (and not `project/Test/test`). This is consistent with the JVM, as ensured by the new scripted test. We pass it down to the `RunConfig` of `JSEnv`s. If a `JSEnv `does not support environment variables, and `envVars.value` is non-empty, the `JSEnv` validator will report an error. --- project/BinaryIncompatibilities.scala | 2 + .../sbtplugin/ScalaJSPluginInternal.scala | 2 + .../src/sbt-test/settings/env-vars/build.sbt | 38 +++++++++++++++++++ .../js/src/main/scala/envvars/Platform.scala | 15 ++++++++ .../jvm/src/main/scala/envvars/Platform.scala | 6 +++ .../env-vars/project/build.properties | 1 + .../settings/env-vars/project/build.sbt | 1 + .../shared/src/main/scala/envvars/Main.scala | 23 +++++++++++ .../src/test/scala/envvars/TestSuite.scala | 27 +++++++++++++ .../src/sbt-test/settings/env-vars/test | 6 +++ .../scalajs/testing/adapter/JSEnvRPC.scala | 3 +- .../scalajs/testing/adapter/TestAdapter.scala | 16 +++++--- 12 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/build.sbt create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/js/src/main/scala/envvars/Platform.scala create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/jvm/src/main/scala/envvars/Platform.scala create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/project/build.properties create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/project/build.sbt create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/shared/src/main/scala/envvars/Main.scala create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/shared/src/test/scala/envvars/TestSuite.scala create mode 100644 sbt-plugin/src/sbt-test/settings/env-vars/test diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index fdf5c4ee56..803503d680 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -25,6 +25,8 @@ object BinaryIncompatibilities { ) val TestAdapter = Seq( + // private[adapter], not an issue + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.testing.adapter.JSEnvRPC.this"), ) val Library = Seq( diff --git a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala index 35678a14e8..72040c0f2f 100644 --- a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala +++ b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala @@ -593,6 +593,7 @@ private[sbtplugin] object ScalaJSPluginInternal { */ val config = RunConfig() .withLogger(sbtLogger2ToolsLogger(log)) + .withEnv((envVars in run).value) .withInheritOut(false) .withInheritErr(false) .withOnOutputStream { (out, err) => @@ -696,6 +697,7 @@ private[sbtplugin] object ScalaJSPluginInternal { val log = streams.value.log val config = TestAdapter.Config() .withLogger(sbtLogger2ToolsLogger(log)) + .withEnv(envVars.value) val adapter = newTestAdapter(env, input, config) val frameworkAdapters = enhanceNotInstalledException(resolvedScoped.value, log) { diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/build.sbt b/sbt-plugin/src/sbt-test/settings/env-vars/build.sbt new file mode 100644 index 0000000000..440ea6877b --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/build.sbt @@ -0,0 +1,38 @@ +inThisBuild(Def.settings( + scalaVersion := "2.12.16", +)) + +lazy val sharedSettings = Def.settings( + Compile / unmanagedSourceDirectories += baseDirectory.value.getParentFile / "shared/src/main/scala", + Test / unmanagedSourceDirectories += baseDirectory.value.getParentFile / "shared/src/test/scala", + + Test / testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-s", "-v"), + + envVars += "PROJECT_ENV_VAR" -> "scoped in project", + run / envVars += "RUN_ENV_VAR" -> "scoped in project/run", // has no effect + Compile / envVars += "COMPILE_ENV_VAR" -> "scoped in project/Compile", + Compile / run / envVars += "COMPILE_RUN_ENV_VAR" -> "scoped in project/Compile/run", + Test / envVars += "TEST_ENV_VAR" -> "scoped in project/Test", + Test / test / envVars += "TEST_TEST_ENV_VAR" -> "scoped in project/Test/test", // has no effect +) + +// Confidence tests on the JVM +lazy val jvmReference = project + .in(file("jvm")) + .settings( + sharedSettings, + libraryDependencies ++= Seq( + "com.novocode" % "junit-interface" % "0.11" % "test", + "junit" % "junit" % "4.13.2" % "test", + ), + fork := true, + ) + +// Actual tests on JS +lazy val jsTests = project + .in(file("js")) + .enablePlugins(ScalaJSPlugin, ScalaJSJUnitPlugin) + .settings( + sharedSettings, + scalaJSUseMainModuleInitializer := true, + ) diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/js/src/main/scala/envvars/Platform.scala b/sbt-plugin/src/sbt-test/settings/env-vars/js/src/main/scala/envvars/Platform.scala new file mode 100644 index 0000000000..d256ff2f92 --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/js/src/main/scala/envvars/Platform.scala @@ -0,0 +1,15 @@ +package envvars + +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +object Platform { + def getEnvVarOpt(envVar: String): Option[String] = + process.env.get(envVar) + + @js.native + @JSGlobal("process") + private object process extends js.Object { + val env: js.Dictionary[String] = js.native + } +} diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/jvm/src/main/scala/envvars/Platform.scala b/sbt-plugin/src/sbt-test/settings/env-vars/jvm/src/main/scala/envvars/Platform.scala new file mode 100644 index 0000000000..38b84a7d61 --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/jvm/src/main/scala/envvars/Platform.scala @@ -0,0 +1,6 @@ +package envvars + +object Platform { + def getEnvVarOpt(envVar: String): Option[String] = + Option(System.getenv(envVar)) +} diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/project/build.properties b/sbt-plugin/src/sbt-test/settings/env-vars/project/build.properties new file mode 100644 index 0000000000..c0bab04941 --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/project/build.sbt b/sbt-plugin/src/sbt-test/settings/env-vars/project/build.sbt new file mode 100644 index 0000000000..7de678c575 --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/project/build.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % sys.props("plugin.version")) diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/shared/src/main/scala/envvars/Main.scala b/sbt-plugin/src/sbt-test/settings/env-vars/shared/src/main/scala/envvars/Main.scala new file mode 100644 index 0000000000..3abb860102 --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/shared/src/main/scala/envvars/Main.scala @@ -0,0 +1,23 @@ +package envvars + +object Main { + def main(args: Array[String]): Unit = { + assertEnvVar("scoped in project", "PROJECT_ENV_VAR") + assertNoEnvVar("RUN_ENV_VAR") + assertEnvVar("scoped in project/Compile", "COMPILE_ENV_VAR") + assertEnvVar("scoped in project/Compile/run", "COMPILE_RUN_ENV_VAR") + assertNoEnvVar("TEST_ENV_VAR") + assertNoEnvVar("TEST_TEST_ENV_VAR") + } + + private def assertNoEnvVar(envVar: String): Unit = + assertEnvVar(None, envVar) + + private def assertEnvVar(expected: String, envVar: String): Unit = + assertEnvVar(Some(expected), envVar) + + private def assertEnvVar(expected: Option[String], envVar: String): Unit = { + val value = Platform.getEnvVarOpt(envVar) + assert(value == expected, s"for $envVar, expected: $expected, but got: $value") + } +} diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/shared/src/test/scala/envvars/TestSuite.scala b/sbt-plugin/src/sbt-test/settings/env-vars/shared/src/test/scala/envvars/TestSuite.scala new file mode 100644 index 0000000000..a1f1c5e93e --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/shared/src/test/scala/envvars/TestSuite.scala @@ -0,0 +1,27 @@ +package envvars + +import org.junit.Test +import org.junit.Assert._ + +class TestSuite { + @Test + def testEnvVars(): Unit = { + assertEnvVar("scoped in project", "PROJECT_ENV_VAR") + assertNoEnvVar("RUN_ENV_VAR") + assertEnvVar("scoped in project/Compile", "COMPILE_ENV_VAR") // because Test "extends" Compile + assertNoEnvVar("COMPILE_RUN_ENV_VAR") + assertEnvVar("scoped in project/Test", "TEST_ENV_VAR") + assertNoEnvVar("TEST_TEST_ENV_VAR") // always ignored + } + + private def assertNoEnvVar(envVar: String): Unit = + assertEnvVar(None, envVar) + + private def assertEnvVar(expected: String, envVar: String): Unit = + assertEnvVar(Some(expected), envVar) + + private def assertEnvVar(expected: Option[String], envVar: String): Unit = { + val value = Platform.getEnvVarOpt(envVar) + assertEquals(s"for $envVar", expected, value) + } +} diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/test b/sbt-plugin/src/sbt-test/settings/env-vars/test new file mode 100644 index 0000000000..0ef55e8652 --- /dev/null +++ b/sbt-plugin/src/sbt-test/settings/env-vars/test @@ -0,0 +1,6 @@ +> show jvmReference/Compile/run/envVars +> jvmReference/run +> show jvmReference/Test/envVars +> jvmReference/test +> jsTests/run +> jsTests/test diff --git a/test-adapter/src/main/scala/org/scalajs/testing/adapter/JSEnvRPC.scala b/test-adapter/src/main/scala/org/scalajs/testing/adapter/JSEnvRPC.scala index dab4394a8a..84cc5d3c06 100644 --- a/test-adapter/src/main/scala/org/scalajs/testing/adapter/JSEnvRPC.scala +++ b/test-adapter/src/main/scala/org/scalajs/testing/adapter/JSEnvRPC.scala @@ -20,7 +20,7 @@ import org.scalajs.testing.common._ /** RPC Core for use with a [[JSEnv]]. */ private[adapter] final class JSEnvRPC( - jsenv: JSEnv, input: Seq[Input], logger: Logger)( + jsenv: JSEnv, input: Seq[Input], logger: Logger, env: Map[String, String])( implicit ec: ExecutionContext) extends RPCCore { private val run: JSComRun = { @@ -35,6 +35,7 @@ private[adapter] final class JSEnvRPC( */ val runConfig = RunConfig() .withLogger(logger) + .withEnv(env) .withInheritOut(false) .withInheritErr(false) .withOnOutputStream { (out, err) => diff --git a/test-adapter/src/main/scala/org/scalajs/testing/adapter/TestAdapter.scala b/test-adapter/src/main/scala/org/scalajs/testing/adapter/TestAdapter.scala index 41f9bee3a6..ae7b7d44dd 100644 --- a/test-adapter/src/main/scala/org/scalajs/testing/adapter/TestAdapter.scala +++ b/test-adapter/src/main/scala/org/scalajs/testing/adapter/TestAdapter.scala @@ -125,7 +125,7 @@ final class TestAdapter(jsEnv: JSEnv, input: Seq[Input], config: TestAdapter.Con // Otherwise we might leak runners. require(!closed, "We are closed. Cannot create new runner.") - val com = new JSEnvRPC(jsEnv, input, config.logger) + val com = new JSEnvRPC(jsEnv, input, config.logger, config.env) val mux = new RunMuxRPC(com) new ManagedRunner(threadId, com, mux) @@ -134,21 +134,27 @@ final class TestAdapter(jsEnv: JSEnv, input: Seq[Input], config: TestAdapter.Con object TestAdapter { final class Config private ( - val logger: Logger + val logger: Logger, + val env: Map[String, String] ) { private def this() = { this( - logger = NullLogger + logger = NullLogger, + env = Map.empty ) } def withLogger(logger: Logger): Config = copy(logger = logger) + def withEnv(env: Map[String, String]): Config = + copy(env = env) + private def copy( - logger: Logger = logger + logger: Logger = logger, + env: Map[String, String] = env ): Config = { - new Config(logger) + new Config(logger, env) } } From d709e1c39d3429688382833d7311b57eedf5cef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 6 Sep 2022 16:10:06 +0200 Subject: [PATCH 65/75] Uncomment a test in FloatTest.parseStringMethods(). It was probably commented out during experimentation, and then forgotten about. --- .../scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala index bbffe2a732..e16c2703d2 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/FloatTest.scala @@ -201,7 +201,7 @@ class FloatTest { // the limit between MaxValue and PositiveInfinity test(Float.MaxValue, "0x1.fffffefp127") - //test(Float.MaxValue, "0x1.fffffeffffffffffffffffffffffffffp127") + test(Float.MaxValue, "0x1.fffffeffffffffffffffffffffffffffp127") test(Float.PositiveInfinity, "0x1.ffffff00000000000000p127") test(Float.PositiveInfinity, "0x1.ffffff00000000000001p127") From 2a4a711f6b8c5c46f136dbc2a2e94d8a1c5a4b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 6 Sep 2022 16:10:48 +0200 Subject: [PATCH 66/75] Un-ignore some tests in BigIntegerConvertTest. They were ignored because they require accurate float semantics. Instead of ignoring them, we now make them `assume` that we have accurate floats. --- .../javalib/math/BigIntegerConvertTest.scala | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala index 2cd0adaa6f..16f5073dd6 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigIntegerConvertTest.scala @@ -21,7 +21,7 @@ package org.scalajs.testsuite.javalib.math import java.math.BigInteger -import org.junit.{Test, Ignore} +import org.junit.Test import org.junit.Assert._ import org.junit.Assume._ @@ -253,14 +253,18 @@ class BigIntegerConvertTest { assertEquals(Float.NegativeInfinity, aNumber, 0.0f) } - @Ignore @Test def testFloatValueNegativeInfinity2(): Unit = { + @Test def testFloatValueNegativeInfinity2(): Unit = { + assumeTrue("requires accurate floats", hasAccurateFloats) + val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = -1 val aNumber = new BigInteger(aSign, a).floatValue() assertEquals(Float.NegativeInfinity, aNumber, 0.0f) } - @Ignore @Test def testFloatValueNegMantissaIsZero(): Unit = { + @Test def testFloatValueNegMantissaIsZero(): Unit = { + assumeTrue("requires accurate floats", hasAccurateFloats) + val a = Array[Byte](1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) val aSign = -1 val aNumber = new BigInteger(aSign, a).floatValue() @@ -294,14 +298,18 @@ class BigIntegerConvertTest { assertTrue(aNumber - result < delta) } - @Ignore @Test def testFloatValuePastNegMaxValue(): Unit = { + @Test def testFloatValuePastNegMaxValue(): Unit = { + assumeTrue("requires accurate floats", hasAccurateFloats) + val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = -1 val aNumber = new BigInteger(aSign, a).floatValue() assertEquals(Float.NegativeInfinity, aNumber, 0.0f) } - @Ignore @Test def testFloatValuePastPosMaxValue(): Unit = { + @Test def testFloatValuePastPosMaxValue(): Unit = { + assumeTrue("requires accurate floats", hasAccurateFloats) + val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = 1 val aNumber = new BigInteger(aSign, a).floatValue() @@ -323,7 +331,9 @@ class BigIntegerConvertTest { assertTrue(aNumber - result < delta) } - @Ignore @Test def testFloatValuePositiveInfinity1(): Unit = { + @Test def testFloatValuePositiveInfinity1(): Unit = { + assumeTrue("requires accurate floats", hasAccurateFloats) + val a = Array[Byte](0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) val aSign = 1 val aNumber: Float = new BigInteger(aSign, a).floatValue() From f3765f9e8cf81e680f4b5f32185e31d7bb1e3aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 6 Sep 2022 17:07:48 +0200 Subject: [PATCH 67/75] Remove dead code j.math.Conversion.bigInteger2Double(). The `BigInteger.doubleValue()` uses `parseDouble(toString())`, not this method. --- .../src/main/scala/java/math/Conversion.scala | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/javalib/src/main/scala/java/math/Conversion.scala b/javalib/src/main/scala/java/math/Conversion.scala index 260996598a..cb8c233ef5 100644 --- a/javalib/src/main/scala/java/math/Conversion.scala +++ b/javalib/src/main/scala/java/math/Conversion.scala @@ -282,37 +282,4 @@ private[math] object Conversion { else result } } - - def bigInteger2Double(bi: BigInteger): Double = { - if (bi.numberLength < 2 || ((bi.numberLength == 2) && (bi.digits(1) > 0))) { - bi.longValue().toDouble - } else if (bi.numberLength > 32) { - if (bi.sign > 0) Double.PositiveInfinity - else Double.NegativeInfinity - } else { - val bitLen = bi.abs().bitLength() - var exponent: Long = bitLen - 1 - val delta = bitLen - 54 - val lVal = bi.abs().shiftRight(delta).longValue() - var mantissa = lVal & 0x1FFFFFFFFFFFFFL - - if (exponent == 1023 && mantissa == 0X1FFFFFFFFFFFFFL) { - if (bi.sign > 0) Double.PositiveInfinity - else Double.NegativeInfinity - } else if (exponent == 1023 && mantissa == 0x1FFFFFFFFFFFFEL) { - if (bi.sign > 0) Double.MaxValue - else -Double.MaxValue - } else { - val droppedBits = BitLevel.nonZeroDroppedBits(delta, bi.digits) - if (((mantissa & 1) == 1) && (((mantissa & 2) == 2) || droppedBits)) - mantissa += 2 - - mantissa >>= 1 - val resSign = if (bi.sign < 0) 0x8000000000000000L else 0 - exponent = ((1023 + exponent) << 52) & 0x7FF0000000000000L - val result = resSign | exponent | mantissa - java.lang.Double.longBitsToDouble(result) - } - } - } } From 5d0d2b0f4780a5d39f66397f60eb5f72364bc1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 6 Sep 2022 17:59:18 +0200 Subject: [PATCH 68/75] Fix #4726: Use parseFloat(toString()) for BigDecimal.floatValue(). Or rather, a faster version thereof, which does not rescale the value to follow the public spec of `toString()`. Similarly, we simplify `doubleValue()` to use `parseDouble()` of the same `toString()`. `parseDouble` is provided by the JS engine. We don't have any evidence either way, but it is not impossible that its native speed compensates for the conversion to string. `parseFloat`, in turn, is implemented on top of `parseDouble`, and only has a fallback for corner cases. Using `parseDouble` and `parseFloat` for `toString()` is already what `BigInteger` does. This is one more reason not to bother to do something cleverer in `BigDecimal`. --- .../src/main/scala/java/math/BigDecimal.scala | 114 +----------------- .../javalib/math/BigDecimalConvertTest.scala | 73 +++++++++++ 2 files changed, 79 insertions(+), 108 deletions(-) diff --git a/javalib/src/main/scala/java/math/BigDecimal.scala b/javalib/src/main/scala/java/math/BigDecimal.scala index 6ee2f36636..98baffe296 100644 --- a/javalib/src/main/scala/java/math/BigDecimal.scala +++ b/javalib/src/main/scala/java/math/BigDecimal.scala @@ -1509,116 +1509,14 @@ class BigDecimal() extends Number with Comparable[BigDecimal] { def byteValueExact(): Byte = valueExact(8).toByte - override def floatValue(): Float = { - /* A similar code like in doubleValue() could be repeated here, - * but this simple implementation is quite efficient. */ - val powerOfTwo = this._bitLength - (_scale / Log2).toLong - val floatResult0: Float = signum().toFloat - val floatResult: Float = { - if (powerOfTwo < -149 || floatResult0 == 0.0f) // 'this' is very small - floatResult0 * 0.0f - else if (powerOfTwo > 129) // 'this' is very large - floatResult0 * Float.PositiveInfinity - else - doubleValue().toFloat - } - floatResult - } - - override def doubleValue(): Double = { - val sign = signum() - val powerOfTwo = this._bitLength - (_scale / Log2).toLong - - if (powerOfTwo < -1074 || sign == 0) { - // Cases which 'this' is very small - sign * 0.0d - } else if (powerOfTwo > 1025) { - // Cases which 'this' is very large - sign * Double.PositiveInfinity - } else { - val mantissa0 = getUnscaledValue.abs() - var exponent = 1076 // bias + 53 - - val mantissa = { - if (_scale <= 0) { - mantissa0.multiply(powerOf10(-_scale)) - } else { - val powerOfTen: BigInteger = powerOf10(_scale) - val k = 100 - powerOfTwo.toInt - val m = { - if (k > 0) { - /* Computing (mantissa * 2^k) , where 'k' is a enough big - * power of '2' to can divide by 10^s */ - exponent -= k - mantissa0.shiftLeft(k) - } else { - mantissa0 - } - } - // Computing (mantissa * 2^k) / 10^s - val qr = m.divideAndRemainderImpl(powerOfTen) - // To check if the fractional part >= 0.5 - val compRem = qr.rem.shiftLeftOneBit().compareTo(powerOfTen) - // To add two rounded bits at end of mantissa - exponent -= 2 - qr.quot.shiftLeft(2).add(BigInteger.valueOf((compRem * (compRem + 3)) / 2 + 1)) - } - } + @noinline override def floatValue(): Float = + java.lang.Float.parseFloat(toStringForFloatingPointValue()) - val lowestSetBit = mantissa.getLowestSetBit() - val discardedSize = mantissa.bitLength() - 54 - var bits: Long = 0L // IEEE-754 Standard - var tempBits: Long = 0L // for temporal calculations - if (discardedSize > 0) { // (#bits > 54) - bits = mantissa.shiftRight(discardedSize).longValue() - tempBits = bits - if (((bits & 1) == 1 && lowestSetBit < discardedSize) || (bits & 3) == 3) - bits += 2 - } else { // (#bits <= 54) - bits = mantissa.longValue() << -discardedSize - tempBits = bits - if ((bits & 3) == 3) - bits += 2 - } - // Testing bit 54 to check if the carry creates a new binary digit - if ((bits & 0x40000000000000L) == 0) { - // To drop the last bit of mantissa (first discarded) - bits >>= 1 - exponent += discardedSize - } else { - // #bits = 54 - bits >>= 2 - exponent += (discardedSize + 1) - } - // To test if the 53-bits number fits in 'double' - if (exponent > 2046) { - // (exponent - bias > 1023) - sign * Double.PositiveInfinity - } else if (exponent < -53) { - sign * 0.0d - } else { - if (exponent <= 0) { - bits = tempBits >> 1 - tempBits = bits & (-1L >>> (63 + exponent)) - bits >>= (-exponent) - // To test if after discard bits, a new carry is generated - if (((bits & 3) == 3) || - (((bits & 1) == 1) && (tempBits != 0) && (lowestSetBit < discardedSize))) { - bits += 1 - } - exponent = 0 - bits >>= 1 - } + @noinline override def doubleValue(): Double = + java.lang.Double.parseDouble(toStringForFloatingPointValue()) - // Construct the 64 double bits: [sign(1), exponent(11), mantissa(52)] - val resultBits = - (sign & 0x8000000000000000L) | - (exponent.toLong << 52) | - (bits & 0xFFFFFFFFFFFFFL) - java.lang.Double.longBitsToDouble(resultBits) - } - } - } + @inline private def toStringForFloatingPointValue(): String = + s"${unscaledValue()}e${-scale()}" def ulp(): BigDecimal = valueOf(1, _scale) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala index 408fbc34a1..57533ab042 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/math/BigDecimalConvertTest.scala @@ -23,8 +23,10 @@ import java.math._ import org.junit.Test import org.junit.Assert._ +import org.junit.Assume._ import org.scalajs.testsuite.utils.AssertThrows.assertThrows +import org.scalajs.testsuite.utils.Platform._ class BigDecimalConvertTest { @@ -71,6 +73,8 @@ class BigDecimalConvertTest { } @Test def testFloatValueNegInfinity(): Unit = { + assumeTrue("requires accurate floats", hasAccurateFloats) + val a = "-123809648392384755735.63567887678287E+200" val aNumber = new BigDecimal(a) val result = Float.NegativeInfinity @@ -85,12 +89,81 @@ class BigDecimalConvertTest { } @Test def testFloatValuePosInfinity(): Unit = { + assumeTrue("requires accurate floats", hasAccurateFloats) + val a = "123809648373567356745735.6356789787678287E+200" val aNumber = new BigDecimal(a) val result = Float.PositiveInfinity assertTrue(aNumber.floatValue() == result) } + /** Test cases for `Float.parseFloat`, with an indirection through `BigDecimal`. */ + @Test def testFloatValueLikeParseFloat_Issue4726(): Unit = { + def test(expected: Float, s: String): Unit = { + if (hasAccurateFloats) { + assertEquals(s, expected: Any, new BigDecimal(s).floatValue()) + } else { + val epsilon = Math.ulp(expected) + assertEquals(s, expected, new BigDecimal(s).floatValue(), epsilon) + } + } + + // Zeros (BigDecimal has no negative 0, so they all parse to +0.0f) + + test(+0.0f, "0") + test(+0.0f, "0.0") + test(+0.0f, "-0.0") + test(+0.0f, "0.e5") + + // Regular values + + test(5.3f, "5.3") + test(12700.0f, "127e2") + test(1.27f, "127E-2") + test(10f, "1E+1") + test(-123.4f, "-123.4") + test(65432.10f, "65432.1") + test(-87654.321f, "-87654.321") + test(0.3f, "+.3") + + // Corner cases that require the BigInteger arithmetics code paths + + test(1.1999999f, "1.199999988079071") // from the bug report + + // k >= 0, e >= 0, f*10^e < m*2^k + test(1.72544037e18f, "1725440439005216752") + test(1.72544037e18f, "1725440439005216767") + + // k >= 0, e >= 0, f*10^e = m*2^k, even is upwards + test(1.72544051e18f, "1725440439005216768") + + // k >= 0, e >= 0, f*10^e > m*2^k + test(1.72544051e18f, "1725440439005216775") + + // k >= 0, e >= 0, f*10^e = m*2^k, even is downwards + test(1.72544051e18f, "1725440576444170240") + test(1.72544051e18f, "172544057644417024e1") + + // k >= 0, e < 0, f*10^e < m*2^k + test(1.72544037e18f, "172544043900521676700000e-5") + + // k < 0, e < 0, f*10^e < m*2^k + test(1.7254404e-18f, "1.725440493251219023E-18") + + /* Attempt at k < 0, e >= 0, f*10^e < m*2^k, but e is adjusted downwards to + * compensate the number of digits after the '.', so it ends up being + * negative anyway. I am not sure we can craft an example that would + * actually use that code path. + */ + test(1.7254404e-18f, "0.00000000000000000000001725440493251219023e5") + + // the limit between MaxValue and PositiveInfinity + test(Float.MaxValue, "3.4028235677973366e38") + test(Float.MaxValue, "3.4028235677973366163e38") + test(Float.PositiveInfinity, "3.4028235677973366164e38") + test(Float.PositiveInfinity, "3.4028235677973367e38") + } + @Test def testIntValueNeg(): Unit = { val a = "-123809648392384754573567356745735.63567890295784902768787678287E+21" val aNumber = new BigDecimal(a) From 7c98a4a2d9a74b56ef327df11bc4553997b6fac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 10 Sep 2022 09:40:46 +0200 Subject: [PATCH 69/75] Eagerly throw UnsupportedEncodingException in URLDecoder.decode. Despite what the documentation says, `decode` eagerly throws when the charset is not supported. This behavior started in JDK 10, when they added the overload of `decode` taking a `Charset`. With that addition, maintaining the lazy behavior would probably have required duplicating the implementation. So in all likelihood, it will not be coming back. It is still throwing as of JDK 17. --- .../src/main/scala/java/net/URLDecoder.scala | 18 ++++++------------ .../scalajs/testsuite/utils/Platform.scala | 2 ++ .../scalajs/testsuite/utils/Platform.scala | 2 ++ .../javalib/net/URLDecoderTest.scala | 19 ++++++++++++------- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/javalib/src/main/scala/java/net/URLDecoder.scala b/javalib/src/main/scala/java/net/URLDecoder.scala index 68e11e617d..c9cc135f40 100644 --- a/javalib/src/main/scala/java/net/URLDecoder.scala +++ b/javalib/src/main/scala/java/net/URLDecoder.scala @@ -21,21 +21,15 @@ import java.nio.charset.{Charset, CharsetDecoder} object URLDecoder { @Deprecated - def decode(s: String): String = decodeImpl(s, () => Charset.defaultCharset()) + def decode(s: String): String = decodeImpl(s, Charset.defaultCharset()) def decode(s: String, enc: String): String = { - decodeImpl(s, { () => - /* An exception is thrown only if the - * character encoding needs to be consulted - */ - if (!Charset.isSupported(enc)) - throw new UnsupportedEncodingException(enc) - else - Charset.forName(enc) - }) + if (!Charset.isSupported(enc)) + throw new UnsupportedEncodingException(enc) + decodeImpl(s, Charset.forName(enc)) } - private def decodeImpl(s: String, getCharset: js.Function0[Charset]): String = { + private def decodeImpl(s: String, charset: Charset): String = { val len = s.length val charBuffer = CharBuffer.allocate(len) @@ -60,7 +54,7 @@ object URLDecoder { case '%' => if (decoder == null) { // equivalent to `byteBuffer == null` - decoder = getCharset().newDecoder() + decoder = charset.newDecoder() byteBuffer = ByteBuffer.allocate(len / 3) } else { byteBuffer.clear() diff --git a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index 4e1adc3cd6..be1e1e28b2 100644 --- a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -26,6 +26,8 @@ object Platform { final val executingInJVMOnJDK8OrLower = false + final val executingInJVMOnLowerThanJDK10 = false + final val executingInJVMOnLowerThanJDK13 = false final val executingInJVMOnLowerThanJDK15 = false diff --git a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index d6575fc33c..8ffe4fcec9 100644 --- a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -24,6 +24,8 @@ object Platform { def executingInJVMOnJDK8OrLower: Boolean = jdkVersion <= 8 + def executingInJVMOnLowerThanJDK10: Boolean = jdkVersion < 10 + def executingInJVMOnLowerThanJDK13: Boolean = jdkVersion < 13 def executingInJVMOnLowerThanJDK15: Boolean = jdkVersion < 15 diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala index 480859c118..483e911764 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala @@ -16,7 +16,7 @@ import org.junit.Test import org.junit.Assert._ import org.scalajs.testsuite.utils.AssertThrows.assertThrows -import org.scalajs.testsuite.utils.Platform.executingInJVM +import org.scalajs.testsuite.utils.Platform._ import java.net.URLDecoder import java.io.UnsupportedEncodingException @@ -77,12 +77,17 @@ class URLDecoderTest { // invalid encoding unsupportedEncoding("2a%20b%20c", enc = "dummy") - // doesn't throw when charset is not needed (not respected by JDK 11) - try { - test("a+b+c", "a b c", enc = "dummy") - } catch { - case th: UnsupportedEncodingException if executingInJVM => - () // ok + /* Throw even if the charset is not needed. + * Despite what the documentation says, `decode` eagerly throws when the + * charset is not supported. This behavior started in JDK 10, when they + * added the overload of `decode` taking a `Charset`. With that addition, + * maintaining the lazy behavior would probably have required duplicating + * the implementation. So in all likelihood, it will not be coming back. + * It is still throwing as of JDK 17. + */ + if (!executingInJVMOnLowerThanJDK10) { + unsupportedEncoding("abc", enc = "dummy") + unsupportedEncoding("a+b+c", enc = "dummy") } // other charsets From 0598b8102813700776ce882592f6f4b40b533f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 10 Sep 2022 09:46:39 +0200 Subject: [PATCH 70/75] Fix #4728: Implement URLDecoder.decode(String, Charset). --- .../src/main/scala/java/net/URLDecoder.scala | 6 +- .../javalib/net/URLDecoderTestOnJDK11.scala | 61 +++++++++++++++ .../javalib/net/URLDecoderTest.scala | 77 ++++++++++++++----- 3 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLDecoderTestOnJDK11.scala diff --git a/javalib/src/main/scala/java/net/URLDecoder.scala b/javalib/src/main/scala/java/net/URLDecoder.scala index c9cc135f40..5bf068cf36 100644 --- a/javalib/src/main/scala/java/net/URLDecoder.scala +++ b/javalib/src/main/scala/java/net/URLDecoder.scala @@ -21,15 +21,15 @@ import java.nio.charset.{Charset, CharsetDecoder} object URLDecoder { @Deprecated - def decode(s: String): String = decodeImpl(s, Charset.defaultCharset()) + def decode(s: String): String = decode(s, Charset.defaultCharset()) def decode(s: String, enc: String): String = { if (!Charset.isSupported(enc)) throw new UnsupportedEncodingException(enc) - decodeImpl(s, Charset.forName(enc)) + decode(s, Charset.forName(enc)) } - private def decodeImpl(s: String, charset: Charset): String = { + def decode(s: String, charset: Charset): String = { val len = s.length val charBuffer = CharBuffer.allocate(len) diff --git a/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLDecoderTestOnJDK11.scala b/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLDecoderTestOnJDK11.scala new file mode 100644 index 0000000000..508fb58b19 --- /dev/null +++ b/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLDecoderTestOnJDK11.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.testsuite.javalib.net + +import org.junit.Test +import org.junit.Assert._ + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets._ +import java.net.URLDecoder + +class URLDecoderTestOnJDK11 { + import URLDecoderTest._ + + @Test + def decodeCharsetCharset(): Unit = { + def test(encoded: String, expected: String, enc: Charset = UTF_8): Unit = + assertEquals(expected, URLDecoder.decode(encoded, enc)) + + def illegalArgumentOrReplacement(encoded: String, enc: Charset = UTF_8): Unit = + testIllegalArgumentOrReplacementGeneric(encoded, URLDecoder.decode(_, enc)) + + // empty string + test("", "") + + // '+' -> ' ' + test("a+b+c", "a b c") + + // single byte codepoint + test("a%20b%20c", "a b c") + + // multi byte codepoint + test("a%c3%9fc", "aßc") + + // consecutive characters + test("a%20%20c", "a c") + + // illegal codepoints + illegalArgumentOrReplacement("a%b%c") + illegalArgumentOrReplacement("%-1") + illegalArgumentOrReplacement("%20%8") + illegalArgumentOrReplacement("%c3%28") + + // other charsets + test("a%20%A3%20c", "a £ c", enc = ISO_8859_1) + test("a%20b%20c", "a b c", enc = US_ASCII) + test("a%00%20b%00%20c", "a b c", enc = UTF_16BE) + test("a%20%00b%20%00c", "a b c", enc = UTF_16LE) + test("a%fe%ff%00%20b%fe%ff%00%20c", "a b c", enc = UTF_16) + } +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala index 483e911764..de8e9dcc66 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLDecoderTest.scala @@ -22,31 +22,45 @@ import java.net.URLDecoder import java.io.UnsupportedEncodingException class URLDecoderTest { + import URLDecoderTest._ - private final val utf8 = "utf-8" - private final val ReplacementChar = '\uFFFD' + @Test + def decodeNoCharset(): Unit = { + def test(encoded: String, expected: String): Unit = + assertEquals(expected, URLDecoder.decode(encoded)) + + def illegalArgumentOrReplacement(encoded: String): Unit = + testIllegalArgumentOrReplacementGeneric(encoded, URLDecoder.decode(_)) + + // empty string + test("", "") + + // '+' -> ' ' + test("a+b+c", "a b c") + + // single byte codepoint + test("a%20b%20c", "a b c") + + // multi byte codepoint + test("a%c3%9fc", "aßc") + + // consecutive characters + test("a%20%20c", "a c") + + // illegal codepoints + illegalArgumentOrReplacement("a%b%c") + illegalArgumentOrReplacement("%-1") + illegalArgumentOrReplacement("%20%8") + illegalArgumentOrReplacement("%c3%28") + } @Test - def decodeTest(): Unit = { + def decodeStringCharset(): Unit = { def test(encoded: String, expected: String, enc: String = utf8): Unit = - assertEquals(URLDecoder.decode(encoded, enc), expected) - - def illegalArgumentOrReplacement(encoded: String, enc: String = utf8): Unit = { - val thrown = { - try { - val res = URLDecoder.decode(encoded, enc) - - /* It is valid to return the Unicode replacement character (U+FFFD) - * when encountering an invalid codepoint. - */ - res.contains(ReplacementChar) - } catch { - case _: IllegalArgumentException => true - } - } + assertEquals(expected, URLDecoder.decode(encoded, enc)) - assertTrue(thrown) - } + def illegalArgumentOrReplacement(encoded: String, enc: String = utf8): Unit = + testIllegalArgumentOrReplacementGeneric(encoded, URLDecoder.decode(_, enc)) def unsupportedEncoding(encoded: String, enc: String = utf8): Unit = { val exception = classOf[UnsupportedEncodingException] @@ -98,3 +112,26 @@ class URLDecoderTest { test("a%fe%ff%00%20b%fe%ff%00%20c", "a b c", enc = "utf-16") } } + +object URLDecoderTest { + private final val utf8 = "utf-8" + private final val ReplacementChar = '\uFFFD' + + @noinline + def testIllegalArgumentOrReplacementGeneric(encoded: String, op: String => String): Unit = { + val thrown = { + try { + val res = op(encoded) + + /* It is valid to return the Unicode replacement character (U+FFFD) + * when encountering an invalid codepoint. + */ + res.contains(ReplacementChar) + } catch { + case _: IllegalArgumentException => true + } + } + + assertTrue(thrown) + } +} From a75651e57948f7a284915c3d649586f2138837bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 3 Sep 2022 12:57:12 +0200 Subject: [PATCH 71/75] Use a single version of the javalib, compiled with Scala 2.12. Now that the entire javalib is independent of Scala (in the produced .sjsir files), we do not need to compile it separately for each version anymore. Instead, we only compile it once, with Scala 2.12, like we do for the linkerPrivateLibrary. At this point, we still embed the produced .sjsir files in the scalajs-library, rather than publishing them on their own. We move the logic related to the minilib to the `javalib` project, since (by design) it only refers to classes of the `javalib`. --- project/Build.scala | 72 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 6978335c0b..08ad52164c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -632,6 +632,22 @@ object Build { else project.dependsOn(compiler.v2_12 % "plugin") } + /** Depends on library2_12 as if (exportJars in library) was set to false. */ + def dependsOnLibraryNoJar2_12: Project = { + val library = LocalProject("library2_12") + if (isGeneratingForIDE) { + project.dependsOn(library) + } else { + project.settings( + internalDependencyClasspath in Compile ++= { + val prods = (products in (library, Compile)).value + val analysis = (compile in (library, Compile)).value + prods.map(p => Classpaths.analyzed(p, analysis)) + } + ) + } + } + def withScalaJSJUnitPlugin2_12: Project = { project.settings( scalacOptions in Test ++= { @@ -965,7 +981,7 @@ object Build { BuildInfoKey.map(previousLibsTask) { case (_, v) => "previousLibs" -> v }, - BuildInfoKey.map(packageMinilib in (library, Compile)) { + BuildInfoKey.map(packageMinilib in (LocalProject("javalib"), Compile)) { case (_, v) => "minilib" -> v.getAbsolutePath }, BuildInfoKey.map(packageBin in (library, Compile)) { @@ -1207,17 +1223,17 @@ object Build { autoScalaLibrary := false, ) - lazy val javalib: MultiScalaProject = MultiScalaProject( + lazy val javalib: Project = Project( id = "javalib", base = file("javalib") ).enablePlugins( MyScalaJSPlugin ).settings( commonSettings, + scalaVersion := DefaultScalaVersion, fatalWarningsSettings, - name := "Java library for Scala.js", + name := "scalajs-javalib", publishArtifact in Compile := false, delambdafySetting, - ensureSAMSupportSetting, recompileAllOrNothingSettings, @@ -1234,24 +1250,6 @@ object Build { // We implement JDK classes, so we emit static forwarders for all static objects scalacOptions ++= scalaJSCompilerOption("genStaticForwardersForNonTopLevelObjects"), - /* In the javalib, we often need to perform `a.equals(b)` with operands - * of unconstrained types in order to implement the JDK specs. Scala - * 2.13.4+ warns for such calls, but those warnings are always noise in - * the javalib, so we globally silence them. - */ - scalacOptions ++= { - val scalaV = scalaVersion.value - val scalaWarnsForNonCooperativeEquals = { - !scalaV.startsWith("2.11.") && - !scalaV.startsWith("2.12.") && - scalaV != "2.13.0" && scalaV != "2.13.1" && scalaV != "2.13.2" && scalaV != "2.13.3" - } - if (scalaWarnsForNonCooperativeEquals) - Seq("-Wconf:cat=other-non-cooperative-equals:s") - else - Nil - }, - // The implementation of java.lang.Object, which is hard-coded in JavaLangObject.scala resourceGenerators in Compile += Def.task { val output = (resourceManaged in Compile).value / "java/lang/Object.sjsir" @@ -1269,8 +1267,19 @@ object Build { !path.contains("/java/math/") && !path.endsWith("/java/util/concurrent/ThreadLocalRandom.scala") } - } - ).withScalaJSCompiler.dependsOnLibraryNoJar + }, + + Compile / packageMinilib := { + val sources = (Compile / packageBin / mappings).value.filter { mapping => + MiniLib.Whitelist.contains(mapping._2.replace('\\', '/')) + } + val jar = crossTarget.value / "minilib.jar" + val config = new sbt.Package.Configuration(sources, jar, Nil) + val s = streams.value + sbt.Package(config, s.cacheStoreFactory, s.log) + jar + }, + ).withScalaJSCompiler2_12.dependsOnLibraryNoJar2_12 lazy val scalalib: MultiScalaProject = MultiScalaProject( id = "scalalib", base = file("scalalib") @@ -1514,7 +1523,7 @@ object Build { */ dependencyClasspath in doc ++= exportedProducts.value, )) - ).zippedSettings(Seq("javalib", "scalalib", "libraryAux"))(localProjects => + ).zippedSettings(Seq("scalalib", "libraryAux"))(localProjects => inConfig(Compile)(Seq( /* Add the .sjsir files from other lib projects * (but not .class files) @@ -1527,23 +1536,12 @@ object Build { val otherProducts = ( (products in localProjects(0)).value ++ (products in localProjects(1)).value ++ - (products in localProjects(2)).value) + (products in LocalProject("javalib")).value) val otherMappings = otherProducts.flatMap(base => Path.selectSubpaths(base, filter)) libraryMappings ++ otherMappings }, - - packageMinilib := { - val sources = (mappings in packageBin).value.filter { mapping => - MiniLib.Whitelist.contains(mapping._2.replace('\\', '/')) - } - val jar = crossTarget.value / "minilib.jar" - val config = new sbt.Package.Configuration(sources, jar, Nil) - val s = streams.value - sbt.Package(config, s.cacheStoreFactory, s.log) - jar - } )) ).withScalaJSCompiler From b737e8088f231af705189d10f0b4f87fd0aa87bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 3 Sep 2022 13:28:03 +0200 Subject: [PATCH 72/75] Use only the javalib in the linker tests. We did not have any test that fundamentally required the full scalajs-library. Some used through `Predef.println`, but they are not fundamentally affected by replacing that with `System.out.println`. Therefore, we reduce the scope of the "fulllib" previously used by the linker tests to the "javalib". This allows to run the LibrarySizeTests in all versions of Scala. More importantly, as we will separate the javalib in a truly different jar, keeping the fulllib would have required to load *two* jars and combine them instead of just one. This reduction in scope allows us not to increase the setup complexity later on. The change of `Predef.println` to `System.out.println` does reduce the surface of IR tested in the backward compat tests and in the test for no `j.l.Class` in a "hello world" app. This is somewhat reducing the "value" of these tests. However, constraining all the tests to the javalib increases the confidence in the javalib being linkable without the scalajs-library. --- .../linker/testutils/StdlibHolder.scala | 2 +- .../scalajs/linker/BackwardsCompatTest.scala | 8 +++---- .../org/scalajs/linker/EmitterTest.scala | 12 +++++----- .../linker/LibraryReachabilityTest.scala | 2 +- .../org/scalajs/linker/LibrarySizeTest.scala | 4 ++-- .../org/scalajs/linker/OptimizerTest.scala | 4 ++-- .../linker/testutils/LinkingUtils.scala | 5 ++++- .../linker/testutils/TestIRBuilder.scala | 9 ++++---- .../scalajs/linker/testutils/TestIRRepo.scala | 2 +- project/Build.scala | 22 ++++++------------- 10 files changed, 33 insertions(+), 37 deletions(-) diff --git a/linker/shared/src/test/scala-ide-stubs/org/scalajs/linker/testutils/StdlibHolder.scala b/linker/shared/src/test/scala-ide-stubs/org/scalajs/linker/testutils/StdlibHolder.scala index 098afbc2be..f2400d0289 100644 --- a/linker/shared/src/test/scala-ide-stubs/org/scalajs/linker/testutils/StdlibHolder.scala +++ b/linker/shared/src/test/scala-ide-stubs/org/scalajs/linker/testutils/StdlibHolder.scala @@ -18,6 +18,6 @@ package org.scalajs.linker.testutils private[testutils] object StdlibHolder { final val minilib = "" - final val fulllib = "" + final val javalib = "" final val previousLibs: Map[String, String] = Map.empty } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala index 986344d839..43b6805e55 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala @@ -40,7 +40,7 @@ class BackwardsCompatTest { @Test def testHelloWorld(): AsyncResult = await { val classDefs = Seq( - mainTestClassDef(predefPrintln(str("Hello world!"))) + mainTestClassDef(systemOutPrintln(str("Hello world!"))) ) test(classDefs, MainTestModuleInitializers) @@ -50,8 +50,8 @@ class BackwardsCompatTest { def testSystemIdentityHashCode(): AsyncResult = await { val classDefs = Seq( mainTestClassDef( - predefPrintln(Apply(EAF, - LoadModule("java.lang.System$"), + systemOutPrintln(ApplyStatic(EAF, + "java.lang.System", m("identityHashCode", List(O), I), List(JSObjectConstr(Nil)))(IntType))) ) @@ -67,7 +67,7 @@ class BackwardsCompatTest { interfaces = List(CloneableClass), memberDefs = List(trivialCtor("A"))), mainTestClassDef( - predefPrintln(Apply(EAF, + systemOutPrintln(Apply(EAF, New("A", NoArgConstructorName, Nil), m("clone", Nil, O), Nil)(AnyType))) ) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala index 3be7c59a4a..17512130bc 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala @@ -87,7 +87,7 @@ class EmitterTest { @Test def linkNoSecondAttemptInEmitter(): AsyncResult = await { val classDefs = List( - mainTestClassDef(predefPrintln(str("Hello world!"))) + mainTestClassDef(systemOutPrintln(str("Hello world!"))) ) val logger = new CapturingLogger @@ -97,8 +97,8 @@ class EmitterTest { val classDefsFiles = classDefs.map(MemClassDefIRFile(_)) for { - fulllib <- TestIRRepo.fulllib - report <- linker.link(fulllib ++ classDefsFiles, + javalib <- TestIRRepo.javalib + report <- linker.link(javalib ++ classDefsFiles, MainTestModuleInitializers, MemOutputDirectory(), logger) } yield { logger.allLogLines.assertNotContains(EmitterSetOfDangerousGlobalRefsChangedMessage) @@ -111,7 +111,7 @@ class EmitterTest { @Test def linkYesSecondAttemptInEmitter(): AsyncResult = await { val classDefs = List( - mainTestClassDef(predefPrintln(JSGlobalRef("$dangerousGlobalRef"))) + mainTestClassDef(systemOutPrintln(JSGlobalRef("$dangerousGlobalRef"))) ) val logger = new CapturingLogger @@ -121,8 +121,8 @@ class EmitterTest { val classDefsFiles = classDefs.map(MemClassDefIRFile(_)) for { - fulllib <- TestIRRepo.fulllib - report <- linker.link(fulllib ++ classDefsFiles, + javalib <- TestIRRepo.javalib + report <- linker.link(javalib ++ classDefsFiles, MainTestModuleInitializers, MemOutputDirectory(), logger) } yield { logger.allLogLines.assertContains(EmitterSetOfDangerousGlobalRefsChangedMessage) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibraryReachabilityTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibraryReachabilityTest.scala index 277a27ff06..8b8981f4cb 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibraryReachabilityTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibraryReachabilityTest.scala @@ -106,7 +106,7 @@ object LibraryReachabilityTest { implicit ec: ExecutionContext): Future[Analysis] = { for { analysis <- LinkingUtils.computeAnalysis(classDefs, symbolRequirements, - moduleInitializers, config, stdlib = TestIRRepo.fulllib) + moduleInitializers, config, stdlib = TestIRRepo.javalib) } yield { if (analysis.errors.nonEmpty) fail(analysis.errors.mkString("Unexpected errors:\n", "\n", "")) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index c9c5290581..57d47b7c22 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -108,8 +108,8 @@ object LibrarySizeTest { val fullOutput = MemOutputDirectory() for { - fulllib <- TestIRRepo.fulllib - irFiles = fulllib ++ classDefsFiles + javalib <- TestIRRepo.javalib + irFiles = javalib ++ classDefsFiles fastLinkReport <- fastLinker.link(irFiles, moduleInitializers, fastOutput, logger) fullLinkReport <- fullLinker.link(irFiles, moduleInitializers, fullOutput, logger) } yield { diff --git a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala index 7415993b39..30bfd2da25 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala @@ -179,13 +179,13 @@ class OptimizerTest { def testHelloWorldDoesNotNeedClassClass(): AsyncResult = await { val classDefs = Seq( mainTestClassDef({ - predefPrintln(str("Hello world!")) + systemOutPrintln(str("Hello world!")) }) ) for { moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers, - stdlib = TestIRRepo.fulllib) + stdlib = TestIRRepo.javalib) } yield { assertFalse(findClass(moduleSet, ClassClass).isDefined) } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala index 2efd2b3d0d..ef09ab821c 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/LinkingUtils.scala @@ -97,12 +97,15 @@ object LinkingUtils { stdlib: Future[Seq[IRFile]] = TestIRRepo.minilib)( implicit ec: ExecutionContext): Future[Analysis] = { + val classDefIRFiles = classDefs.map(MemClassDefIRFile(_)) + val injectedIRFiles = StandardLinkerBackend(config).injectedIRFiles + val loader = new IRLoader(checkIR = true) val logger = new ScalaConsoleLogger(Level.Error) for { baseFiles <- stdlib - irLoader <- loader.update(classDefs.map(MemClassDefIRFile(_)) ++ baseFiles, logger) + irLoader <- loader.update(classDefIRFiles ++ baseFiles ++ injectedIRFiles, logger) analysis <- Analyzer.computeReachability( CommonPhaseConfig.fromStandardConfig(config), moduleInitializers, symbolRequirements, allowAddingSyntheticMethods = true, diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala index 4fae324104..5f8f1b67e3 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala @@ -108,12 +108,13 @@ object TestIRBuilder { def consoleLog(expr: Tree): Tree = JSMethodApply(JSGlobalRef("console"), str("log"), List(expr)) - def predefPrintln(expr: Tree): Tree = { - val PredefModuleClass = ClassName("scala.Predef$") + def systemOutPrintln(expr: Tree): Tree = { + val PrintStreamClass = ClassName("java.io.PrintStream") + val outMethodName = m("out", Nil, ClassRef(PrintStreamClass)) val printlnMethodName = m("println", List(O), VoidRef) - Apply(EAF, LoadModule(PredefModuleClass), printlnMethodName, List(expr))( - NoType) + val out = ApplyStatic(EAF, "java.lang.System", outMethodName, Nil)(ClassType(PrintStreamClass)) + Apply(EAF, out, printlnMethodName, List(expr))(NoType) } def paramDef(name: LocalName, ptpe: Type): ParamDef = diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala index 84e3eec65d..a32485b8ce 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala @@ -20,7 +20,7 @@ import org.scalajs.linker.interface.IRFile object TestIRRepo { val minilib: Future[Seq[IRFile]] = load(StdlibHolder.minilib) - val fulllib: Future[Seq[IRFile]] = load(StdlibHolder.fulllib) + val javalib: Future[Seq[IRFile]] = load(StdlibHolder.javalib) val empty: Future[Seq[IRFile]] = Future.successful(Nil) val previousLibs: Map[String, Future[Seq[IRFile]]] = StdlibHolder.previousLibs.map(x => x._1 -> load(x._2)) diff --git a/project/Build.scala b/project/Build.scala index 08ad52164c..27e12250e3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -941,7 +941,7 @@ object Build { library.v2_12, jUnitRuntime.v2_12 % "test", testBridge.v2_12 % "test", ) - def commonLinkerSettings(library: LocalProject) = Def.settings( + def commonLinkerSettings: Seq[Setting[_]] = Def.settings( commonSettings, publishSettings(None), fatalWarningsSettings, @@ -984,8 +984,8 @@ object Build { BuildInfoKey.map(packageMinilib in (LocalProject("javalib"), Compile)) { case (_, v) => "minilib" -> v.getAbsolutePath }, - BuildInfoKey.map(packageBin in (library, Compile)) { - case (_, v) => "fulllib" -> v.getAbsolutePath + BuildInfoKey.map(packageBin in (LocalProject("javalib"), Compile)) { + case (_, v) => "javalib" -> v.getAbsolutePath }, ) }, @@ -1009,21 +1009,13 @@ object Build { exportJars := true, // required so ScalaDoc linking works testOptions += Tests.Argument(TestFrameworks.JUnit, "-a"), - - // Execute LibrarySizeTest only for the default Scala version of the build - testOptions ++= { - if (scalaVersion.value == DefaultScalaVersion) - Nil - else - Seq(Tests.Filter(s => !s.endsWith("LibrarySizeTest"))) - }, ) lazy val linker: MultiScalaProject = MultiScalaProject( id = "linker", base = file("linker/jvm") - ).zippedSettings("library")( - commonLinkerSettings _ ).settings( + commonLinkerSettings, + libraryDependencies ++= Seq( "com.google.javascript" % "closure-compiler" % "v20220202", "com.google.jimfs" % "jimfs" % "1.1" % "test", @@ -1055,9 +1047,9 @@ object Build { id = "linkerJS", base = file("linker/js") ).enablePlugins( MyScalaJSPlugin - ).zippedSettings("library")( - commonLinkerSettings _ ).settings( + commonLinkerSettings, + Test / scalacOptions ++= scalaJSCompilerOption("nowarnGlobalExecutionContext"), buildInfoOrStubs(Compile, Def.setting( From efe177b0405ff0f2ada3ef8036d6984e9e49d704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 5 Sep 2022 19:36:41 +0200 Subject: [PATCH 73/75] Separate the `javalib` into its own artifact. Previously, the .sjsir files produced for the `javalib` were included in the `scalajs-library`. Now, instead, we publish the `javalib` as its own artifact, and we make the `scalajs-library` depend on it. --- The dependency is tricky to set up, because, at *compile*-time, the `javalib` depends on `library2_12`. To break the cycle, we decompose the logical `javalib` in two different contrete projects: `javalibInternal` for *compilation*, and `javalib` (public) for *packaging* and *publication*. The `javalibInternal` contains actual sources, and depends on the `library` via the "no-jar" variant (as before). It is not published, and nothing dependsOn it. The public `javalib` has no dependency and is set up like a Java project (so no dependency on the scalalib either). It contains no source. However, it takes its `mappings` from the `javalibInternal`, so its jar contains the products of the `javalibInternal`. Because if has no dependency, we can safely have the `library` dependsOn it, which sets up all the correct dependency information in the published POM files. --- Having a unique, independent `scalajs-javalib` artifact would allow other languages to target the Scala.js IR and the `javalib`, without depending on a particular version of the Scala library. There are currently no concrete plan like this, but it opens the door to such a plan. --- build.sbt | 1 + project/Build.scala | 173 +++++++++++++++++++++++++++++++++----------- scripts/publish.sh | 2 +- 3 files changed, 134 insertions(+), 42 deletions(-) diff --git a/build.sbt b/build.sbt index d1ad06c8fc..e3374e3358 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,7 @@ val linkerJS = Build.linkerJS val testAdapter = Build.testAdapter val sbtPlugin = Build.plugin val javalibintf = Build.javalibintf +val javalibInternal = Build.javalibInternal val javalib = Build.javalib val scalalib = Build.scalalib val libraryAux = Build.libraryAux diff --git a/project/Build.scala b/project/Build.scala index 27e12250e3..28d77af217 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -536,10 +536,10 @@ object Build { * - `"semver-spec"` for artifacts whose public API can only break in major releases (e.g., `library`) * * At the moment, we only set the version scheme for artifacts in the - * "library ecosystem", i.e., scalajs-library, scalajs-test-interface, - * scalajs-junit-runtime and scalajs-test-bridge. Artifacts of the "tools - * ecosystem" do not have a version scheme set, as the jury is still out on - * what is the best way to specify them. + * "library ecosystem", i.e., scalajs-javalib scalajs-library, + * scalajs-test-interface, scalajs-junit-runtime and scalajs-test-bridge. + * Artifacts of the "tools ecosystem" do not have a version scheme set, as + * the jury is still out on what is the best way to specify them. * * See also https://www.scala-sbt.org/1.x/docs/Publishing.html#Version+scheme */ @@ -648,6 +648,32 @@ object Build { } } + /** Depends on library and, by artificial transitivity, on the javalib. */ + def dependsOnLibrary2_12: Project = { + val library = LocalProject("library2_12") + + // Add a real dependency on the library + val project1 = project + .dependsOn(library) + + /* Because the javalib's exportsJar is false, but its actual products are + * only in its jar, we must manually add the jar on the internal + * classpath. + * Once published, only jars are ever used, so this is fine. + */ + if (isGeneratingForIDE) { + project1 + } else { + project1 + .settings( + Compile / internalDependencyClasspath += + (javalib / Compile / packageBin).value, + Test / internalDependencyClasspath += + (javalib / Compile / packageBin).value, + ) + } + } + def withScalaJSJUnitPlugin2_12: Project = { project.settings( scalacOptions in Test ++= { @@ -691,6 +717,31 @@ object Build { } } + /** Depends on library and, by transitivity, on the javalib. */ + def dependsOnLibrary: MultiScalaProject = { + // Add a real dependency on the library + val project1 = project + .dependsOn(library) + + /* Because the javalib's exportsJar is false, but its actual products are + * only in its jar, we must manually add the jar on the internal + * classpath. + * Once published, only jars are ever used, so this is fine. + */ + if (isGeneratingForIDE) { + project1 + } else { + // Actually add classpath dependencies on the javalib jar + project1 + .settings( + Compile / internalDependencyClasspath += + (javalib / Compile / packageBin).value, + Test / internalDependencyClasspath += + (javalib / Compile / packageBin).value, + ) + } + } + /** Depends on the sources of another project. */ def dependsOnSource(dependency: MultiScalaProject): MultiScalaProject = { if (isGeneratingForIDE) { @@ -730,7 +781,7 @@ object Build { linkerInterface, linkerInterfaceJS, linker, linkerJS, testAdapter, javalibintf, - javalib, scalalib, libraryAux, library, + javalibInternal, javalib, scalalib, libraryAux, library, testInterface, jUnitRuntime, testBridge, jUnitPlugin, jUnitAsyncJS, jUnitAsyncJVM, jUnitTestOutputsJS, jUnitTestOutputsJVM, helloworld, reversi, testingExample, testSuite, testSuiteJVM, @@ -792,8 +843,8 @@ object Build { MyScalaJSPlugin ).settings( commonIrProjectSettings, - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( - library, jUnitRuntime % "test", testBridge % "test" + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + jUnitRuntime % "test", testBridge % "test" ) lazy val compiler: MultiScalaProject = MultiScalaProject( @@ -923,8 +974,8 @@ object Build { fileSet.toSeq.filter(_.getPath().endsWith(".scala")) }.taskValue, - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( - library, irProjectJS, jUnitRuntime % "test", testBridge % "test", jUnitAsyncJS % "test", + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + irProjectJS, jUnitRuntime % "test", testBridge % "test", jUnitAsyncJS % "test", ) lazy val linkerPrivateLibrary: Project = (project in file("linker-private-library")).enablePlugins( @@ -937,8 +988,8 @@ object Build { publishArtifact in Compile := false, delambdafySetting, cleanIRSettings - ).withScalaJSCompiler2_12.withScalaJSJUnitPlugin2_12.dependsOn( - library.v2_12, jUnitRuntime.v2_12 % "test", testBridge.v2_12 % "test", + ).withScalaJSCompiler2_12.withScalaJSJUnitPlugin2_12.dependsOnLibrary2_12.dependsOn( + jUnitRuntime.v2_12 % "test", testBridge.v2_12 % "test", ) def commonLinkerSettings: Seq[Setting[_]] = Def.settings( @@ -959,6 +1010,7 @@ object Build { buildInfoPackage in Test := "org.scalajs.linker.testutils", buildInfoObject in Test := "StdlibHolder", buildInfoOptions in Test += BuildInfoOption.PackagePrivate, + buildInfoKeys in Test := { val previousLibsTask = Def.task { val s = streams.value @@ -969,7 +1021,13 @@ object Build { val retrieveDir = s.cacheDirectory / "previous-stdlibs" previousVersions.map { version => - val jars = lm.retrieve("org.scala-js" % s"scalajs-library_$binVer" % version intransitive(), + // Prior to Scala.js 1.11.0, the javalib IR files were in scalajs-library + val artifactName = CrossVersion.partialVersion(version) match { + case Some((1L, minor)) if minor < 11 => s"scalajs-library_$binVer" + case _ => "scalajs-javalib" + } + + val jars = lm.retrieve("org.scala-js" % artifactName % version intransitive(), scalaModuleInfo = None, retrieveDir, log) .fold(w => throw w.resolveException, _.distinct) assert(jars.size == 1, jars.toString()) @@ -1079,8 +1137,8 @@ object Build { }, scalaJSLinkerConfig in Test ~= (_.withModuleKind(ModuleKind.CommonJSModule)) - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( - linkerInterfaceJS, library, irProjectJS, jUnitRuntime % "test", testBridge % "test", jUnitAsyncJS % "test" + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + linkerInterfaceJS, irProjectJS, jUnitRuntime % "test", testBridge % "test", jUnitAsyncJS % "test" ) lazy val testAdapter: MultiScalaProject = MultiScalaProject( @@ -1149,6 +1207,8 @@ object Build { publishLocal in jUnitPlugin.v2_13, // JS libs + publishLocal in javalib, + publishLocal in library.v2_11, publishLocal in testInterface.v2_11, publishLocal in testBridge.v2_11, @@ -1215,15 +1275,20 @@ object Build { autoScalaLibrary := false, ) - lazy val javalib: Project = Project( - id = "javalib", base = file("javalib") + /** The project that actually compiles the `javalib`, but which is not + * exposed. + * + * Instead, its products are copied in `javalib`. + */ + lazy val javalibInternal: Project = Project( + id = "javalibInternal", base = file("javalib") ).enablePlugins( MyScalaJSPlugin ).settings( commonSettings, scalaVersion := DefaultScalaVersion, fatalWarningsSettings, - name := "scalajs-javalib", + name := "scalajs-javalib-internal", publishArtifact in Compile := false, delambdafySetting, @@ -1253,6 +1318,12 @@ object Build { cleanIRSettings, + Compile / doc := { + val dir = (Compile / doc / target).value + IO.createDirectory(dir) + dir + }, + headerSources in Compile ~= { srcs => srcs.filter { src => val path = src.getPath.replace('\\', '/') @@ -1260,6 +1331,27 @@ object Build { !path.endsWith("/java/util/concurrent/ThreadLocalRandom.scala") } }, + ).withScalaJSCompiler2_12.dependsOnLibraryNoJar2_12 + + /** An empty project, without source nor dependencies, whose products are + * copied from `javalibInternal`. + * + * This the "public" version of the javalib, as depended on by the `library` + * and published on Maven. + */ + lazy val javalib: Project = Project( + id = "javalib", base = file("javalib-public") + ).settings( + commonSettings, + name := "scalajs-javalib", + publishSettings(Some(VersionScheme.BreakOnMajor)), + + crossPaths := false, + autoScalaLibrary := false, + crossVersion := CrossVersion.disabled, + + Compile / packageBin / mappings := (javalibInternal / Compile / packageBin / mappings).value, + exportJars := false, // very important, otherwise there's a cycle with the `library` Compile / packageMinilib := { val sources = (Compile / packageBin / mappings).value.filter { mapping => @@ -1271,7 +1363,7 @@ object Build { sbt.Package(config, s.cacheStoreFactory, s.log) jar }, - ).withScalaJSCompiler2_12.dependsOnLibraryNoJar2_12 + ) lazy val scalalib: MultiScalaProject = MultiScalaProject( id = "scalalib", base = file("scalalib") @@ -1441,7 +1533,7 @@ object Build { ).enablePlugins( MyScalaJSPlugin ).dependsOn( - javalibintf % Provided, + javalibintf % Provided, javalib, ).settings( commonSettings, publishSettings(Some(VersionScheme.BreakOnMajor)), @@ -1527,8 +1619,7 @@ object Build { val otherProducts = ( (products in localProjects(0)).value ++ - (products in localProjects(1)).value ++ - (products in LocalProject("javalib")).value) + (products in localProjects(1)).value) val otherMappings = otherProducts.flatMap(base => Path.selectSubpaths(base, filter)) @@ -1551,7 +1642,7 @@ object Build { delambdafySetting, previousArtifactSetting, mimaBinaryIssueFilters ++= BinaryIncompatibilities.TestInterface - ).withScalaJSCompiler.dependsOn(library) + ).withScalaJSCompiler.dependsOnLibrary lazy val testBridge: MultiScalaProject = MultiScalaProject( id = "testBridge", base = file("test-bridge") @@ -1574,8 +1665,8 @@ object Build { baseDirectory.value.getParentFile.getParentFile / "test-common/src/main/scala", unmanagedSourceDirectories in Test += baseDirectory.value.getParentFile.getParentFile / "test-common/src/test/scala" - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( - library, testInterface, jUnitRuntime % "test", jUnitAsyncJS % "test" + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + testInterface, jUnitRuntime % "test", jUnitAsyncJS % "test" ) lazy val jUnitRuntime: MultiScalaProject = MultiScalaProject( @@ -1597,7 +1688,7 @@ object Build { !path.contains("/org/junit/") && !path.contains("/org/hamcrest/") } } - ).withScalaJSCompiler.dependsOn(testInterface) + ).withScalaJSCompiler.dependsOnLibrary.dependsOn(testInterface) val commonJUnitTestOutputsSettings = Def.settings( commonSettings, @@ -1618,7 +1709,7 @@ object Build { ).settings( commonJUnitTestOutputsSettings, name := "Tests for Scala.js JUnit output in JS." - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( jUnitRuntime % "test", testBridge % "test", jUnitAsyncJS % "test" ) @@ -1657,7 +1748,7 @@ object Build { fatalWarningsSettings, name := "Scala.js internal JUnit async JS support", publishArtifact in Compile := false - ).dependsOn(library) + ).dependsOnLibrary lazy val jUnitAsyncJVM: MultiScalaProject = MultiScalaProject( id = "jUnitAsyncJVM", base = file("junit-async/jvm") @@ -1684,7 +1775,7 @@ object Build { name := "Hello World - Scala.js example", moduleName := "helloworld", scalaJSUseMainModuleInitializer := true - ).withScalaJSCompiler.dependsOn(library) + ).withScalaJSCompiler.dependsOnLibrary lazy val reversi: MultiScalaProject = MultiScalaProject( id = "reversi", base = file("examples") / "reversi" @@ -1735,7 +1826,7 @@ object Build { None } } - ).withScalaJSCompiler.dependsOn(library) + ).withScalaJSCompiler.dependsOnLibrary lazy val testingExample: MultiScalaProject = MultiScalaProject( id = "testingExample", base = file("examples") / "testing" @@ -1751,8 +1842,8 @@ object Build { "testingExample/test is not supported because it requires DOM " + "support. Use testingExample/testHtml instead.") } - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( - library, jUnitRuntime % "test", testBridge % "test" + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + jUnitRuntime % "test", testBridge % "test" ) // Testing @@ -2096,8 +2187,8 @@ object Build { }, ).zippedSettings(testSuiteLinker)( l => inConfig(Bootstrap)(testSuiteBootstrapSetting(l)) - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( - library, jUnitRuntime, testBridge % "test", jUnitAsyncJS % "test" + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + jUnitRuntime, testBridge % "test", jUnitAsyncJS % "test" ) lazy val testSuiteJVM: MultiScalaProject = MultiScalaProject( @@ -2158,9 +2249,7 @@ object Build { scalacOptions += "-Yno-predef", // We implement JDK classes, so we emit static forwarders for all static objects scalacOptions ++= scalaJSCompilerOption("genStaticForwardersForNonTopLevelObjects"), - ).withScalaJSCompiler.dependsOn( - library - ) + ).withScalaJSCompiler.dependsOnLibrary def testSuiteExCommonSettings(isJSTest: Boolean): Seq[Setting[_]] = Def.settings( publishArtifact in Compile := false, @@ -2194,8 +2283,8 @@ object Build { testSuiteExCommonSettings(isJSTest = true), name := "Scala.js test suite ex", publishArtifact in Compile := false, - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn( - library, javalibExtDummies, jUnitRuntime, testBridge % "test", testSuite + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + javalibExtDummies, jUnitRuntime, testBridge % "test", testSuite ) lazy val testSuiteExJVM: MultiScalaProject = MultiScalaProject( @@ -2233,7 +2322,7 @@ object Build { baseDirectory.value.getParentFile.getParentFile / "project/TestSuiteLinkerOptions.scala" } - ).withScalaJSCompiler.dependsOn(linkerJS) + ).withScalaJSCompiler.dependsOnLibrary.dependsOn(linkerJS) def shouldPartestSetting(partestSuite: LocalProject) = Def.settings( shouldPartest := { @@ -2374,7 +2463,7 @@ object Build { }.value ).zippedSettings("partestSuite")(partestSuite => shouldPartestSetting(partestSuite) - ).dependsOn(partest % "test", library) + ).dependsOnLibrary.dependsOn(partest % "test") lazy val scalaTestSuite: MultiScalaProject = MultiScalaProject( id = "scalaTestSuite", base = file("scala-test-suite") @@ -2432,6 +2521,8 @@ object Build { case (rel, file) if !blacklist.contains(rel) => file } } - ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOn(jUnitRuntime, testBridge % "test") + ).withScalaJSCompiler.withScalaJSJUnitPlugin.dependsOnLibrary.dependsOn( + jUnitRuntime, testBridge % "test" + ) } diff --git a/scripts/publish.sh b/scripts/publish.sh index 4c7aee070a..03b7e83c08 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -9,7 +9,7 @@ fi SUFFIXES="2_11 2_12 2_13" -JAVA_LIBS="javalibintf" +JAVA_LIBS="javalibintf javalib" COMPILER="compiler jUnitPlugin" JS_LIBS="library irJS linkerInterfaceJS linkerJS testInterface testBridge jUnitRuntime" JVM_LIBS="ir linkerInterface linker testAdapter" From 3c9b917b0ef8e75e8d7e454df2534f326fd8dce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 10 Sep 2022 11:09:32 +0200 Subject: [PATCH 74/75] Implement java.net.URLEncoder. Since we have URLDecoder, it makes sense to also provide URLEncoder. --- .../src/main/scala/java/net/URLEncoder.scala | 121 ++++++++++++++++++ .../src/main/scala/java/util/ScalaOps.scala | 15 +++ .../javalib/net/URLEncoderTestOnJDK11.scala | 57 +++++++++ .../javalib/net/URLEncoderTest.scala | 99 ++++++++++++++ 4 files changed, 292 insertions(+) create mode 100644 javalib/src/main/scala/java/net/URLEncoder.scala create mode 100644 test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLEncoderTestOnJDK11.scala create mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLEncoderTest.scala diff --git a/javalib/src/main/scala/java/net/URLEncoder.scala b/javalib/src/main/scala/java/net/URLEncoder.scala new file mode 100644 index 0000000000..1f9d200b50 --- /dev/null +++ b/javalib/src/main/scala/java/net/URLEncoder.scala @@ -0,0 +1,121 @@ +/* + * 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.net + +import scala.annotation.switch + +import java.io.UnsupportedEncodingException +import java.nio.{CharBuffer, ByteBuffer} +import java.nio.charset.{Charset, CharsetDecoder} + +import java.util.ScalaOps._ +import java.nio.charset.CodingErrorAction + +object URLEncoder { + private final val EncodeAsIsLength = 128 + + private val EncodedAsIs: Array[Boolean] = { + val r = new Array[Boolean](EncodeAsIsLength) // initialized with false + r('.') = true + r('-') = true + r('*') = true + r('_') = true + for (c <- '0'.toInt to '9'.toInt) + r(c) = true + for (c <- 'A'.toInt to 'Z'.toInt) + r(c) = true + for (c <- 'a'.toInt to 'z'.toInt) + r(c) = true + r + } + + private val PercentEncoded: Array[String] = { + val hexDigits = "0123456789ABCDEF" + val r = new Array[String](256) + for (b <- 0 until 256) + r(b) = "%" + hexDigits.charAt(b >>> 4) + hexDigits.charAt(b & 0xf) + r + } + + @Deprecated + def encode(s: String): String = encode(s, Charset.defaultCharset()) + + def encode(s: String, enc: String): String = { + if (!Charset.isSupported(enc)) + throw new UnsupportedEncodingException(enc) + encode(s, Charset.forName(enc)) + } + + def encode(s: String, charset: Charset): String = { + val EncodedAsIs = this.EncodedAsIs // local copy + + @inline def encodeAsIs(c: Char): Boolean = + c < EncodeAsIsLength && EncodedAsIs(c) + + @inline def encodeUsingCharset(c: Char): Boolean = + c != ' ' && !encodeAsIs(c) + + var len = s.length() + var i = 0 + + while (i != len && encodeAsIs(s.charAt(i))) + i += 1 + + if (i == len) { + s + } else { + val PercentEncoded = this.PercentEncoded // local copy + + val charBuffer = CharBuffer.wrap(s) + val encoder = charset.newEncoder().onUnmappableCharacter(CodingErrorAction.REPLACE) + val bufferArray = new Array[Byte](((len - i + 1) * encoder.maxBytesPerChar()).toInt) + val buffer = ByteBuffer.wrap(bufferArray) + + var result = s.substring(0, i) + + while (i != len) { + val startOfChunk = i + val firstChar = s.charAt(startOfChunk) + i += 1 + + if (encodeAsIs(firstChar)) { + // A chunk of characters encoded as is + while (i != len && encodeAsIs(s.charAt(i))) + i += 1 + result += s.substring(startOfChunk, i) + } else if (firstChar == ' ') { + // A single ' ' + result += "+" + } else { + /* A chunk of characters to encode using the charset. + * + * Encoding as big a chunk as possible is not only good for + * performance. It allows us to deal with surrogate pairs without + * additional logic. + */ + while (i != len && encodeUsingCharset(s.charAt(i))) + i += 1 + charBuffer.limit(i) // must be done before setting position + charBuffer.position(startOfChunk) + buffer.rewind() + encoder.reset().encode(charBuffer, buffer, true) + for (j <- 0 until buffer.position()) + result += PercentEncoded(bufferArray(j) & 0xff) + } + } + + result + } + } + +} diff --git a/javalib/src/main/scala/java/util/ScalaOps.scala b/javalib/src/main/scala/java/util/ScalaOps.scala index a3c64920b3..32023f2ab7 100644 --- a/javalib/src/main/scala/java/util/ScalaOps.scala +++ b/javalib/src/main/scala/java/util/ScalaOps.scala @@ -18,6 +18,9 @@ private[java] object ScalaOps { implicit class IntScalaOps private[ScalaOps] (val __self: Int) extends AnyVal { @inline def until(end: Int): SimpleRange = new SimpleRange(__self, end) + + @inline def to(end: Int): SimpleInclusiveRange = + new SimpleInclusiveRange(__self, end) } @inline @@ -32,6 +35,18 @@ private[java] object ScalaOps { } } + @inline + final class SimpleInclusiveRange(start: Int, end: Int) { + @inline + def foreach[U](f: Int => U): Unit = { + var i = start + while (i <= end) { + f(i) + i += 1 + } + } + } + implicit class ToJavaIterableOps[A] private[ScalaOps] ( val __self: java.lang.Iterable[A]) extends AnyVal { diff --git a/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLEncoderTestOnJDK11.scala b/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLEncoderTestOnJDK11.scala new file mode 100644 index 0000000000..d286f21286 --- /dev/null +++ b/test-suite/shared/src/test/require-jdk11/org/scalajs/testsuite/javalib/net/URLEncoderTestOnJDK11.scala @@ -0,0 +1,57 @@ +/* + * 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.testsuite.javalib.net + +import org.junit.Test +import org.junit.Assert._ + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets._ +import java.net.URLEncoder + +class URLEncoderTestOnJDK11 { + @Test + def encodeCharsetCharset(): Unit = { + def test(unencoded: String, expected: String, enc: Charset = UTF_8): Unit = + assertEquals(expected, URLEncoder.encode(unencoded, enc)) + + // empty string + test("", "") + + // . - * _ remain the same, as well as ASCII letters and digits + test("afz-AFZ-069-.*_", "afz-AFZ-069-.*_") + + // ' ' -> '+' + test("a b c", "a+b+c") + + // single byte codepoint + test("a@b#c", "a%40b%23c") + + // multi byte codepoint, include surrogate pairs + test("aßcd\uD834\uDD1Eé", "a%C3%9Fcd%F0%9D%84%9E%C3%A9") // 𝄞 U+1D11E MUSICAL SYMBOL G CLEF + + // consecutive characters + test("aß#c", "a%C3%9F%23c") + + // other charsets + test("a-£-#-é-ß-c", "a-%C2%A3-%23-%C3%A9-%C3%9F-c", enc = UTF_8) + test("a-£-#-é-ß-c", "a-%A3-%23-%E9-%DF-c", enc = ISO_8859_1) + test("a-£-#-é-ß-c", "a-%3F-%23-%3F-%3F-c", enc = US_ASCII) + test("a-£-#-é-ß-c", "a-%00%A3-%00%23-%00%E9-%00%DF-c", enc = UTF_16BE) + test("a-£-#-é-ß-c", "a-%A3%00-%23%00-%E9%00-%DF%00-c", enc = UTF_16LE) + + /* Do not test with UTF_16 becauses it introduces BOM's in the middle of + * the encoded string, which is nonsensical. + */ + } +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLEncoderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLEncoderTest.scala new file mode 100644 index 0000000000..3de8c75fba --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/net/URLEncoderTest.scala @@ -0,0 +1,99 @@ +/* + * 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.testsuite.javalib.net + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows + +import java.net.URLEncoder +import java.io.UnsupportedEncodingException + +class URLEncoderTest { + private final val utf8 = "utf-8" + + @Test + def encodeNoCharset(): Unit = { + def test(unencoded: String, expected: String): Unit = + assertEquals(expected, URLEncoder.encode(unencoded)) + + // empty string + test("", "") + + // . - * _ remain the same, as well as ASCII letters and digits + test("afz-AFZ-069-.*_", "afz-AFZ-069-.*_") + + // ' ' -> '+' + test("a b c", "a+b+c") + + // single byte codepoint + test("a@b#c", "a%40b%23c") + + // multi byte codepoint, include surrogate pairs + test("aßcd\uD834\uDD1Eé", "a%C3%9Fcd%F0%9D%84%9E%C3%A9") // 𝄞 U+1D11E MUSICAL SYMBOL G CLEF + + // consecutive characters + test("aß#c", "a%C3%9F%23c") + } + + @Test + def encodeStringCharset(): Unit = { + def test(unencoded: String, expected: String, enc: String = utf8): Unit = + assertEquals(expected, URLEncoder.encode(unencoded, enc)) + + def unsupportedEncoding(unencoded: String, enc: String = utf8): Unit = { + val exception = classOf[UnsupportedEncodingException] + assertThrows(exception, URLEncoder.encode(unencoded, enc)) + } + + // empty string + test("", "") + + // . - * _ remain the same, as well as ASCII letters and digits + test("afz-AFZ-069-.*_", "afz-AFZ-069-.*_") + + // ' ' -> '+' + test("a b c", "a+b+c") + + // single byte codepoint + test("a@b#c", "a%40b%23c") + + // multi byte codepoint, include surrogate pairs + test("aßcd\uD834\uDD1Eé", "a%C3%9Fcd%F0%9D%84%9E%C3%A9") // 𝄞 U+1D11E MUSICAL SYMBOL G CLEF + + // consecutive characters + test("aß#c", "a%C3%9F%23c") + + // invalid encoding + unsupportedEncoding("2a%20b%20c", enc = "dummy") + + /* Throw even if the charset is not needed. + * Unlike for URLDecoder.decode, this is really specified, and even JDK 8 + * behaves like that. + */ + unsupportedEncoding("abc", enc = "dummy") + unsupportedEncoding("a+b+c", enc = "dummy") + + // other charsets + test("a-£-#-é-ß-c", "a-%C2%A3-%23-%C3%A9-%C3%9F-c", enc = "utf-8") + test("a-£-#-é-ß-c", "a-%A3-%23-%E9-%DF-c", enc = "iso-8859-1") + test("a-£-#-é-ß-c", "a-%3F-%23-%3F-%3F-c", enc = "us-ascii") + test("a-£-#-é-ß-c", "a-%00%A3-%00%23-%00%E9-%00%DF-c", enc = "utf-16be") + test("a-£-#-é-ß-c", "a-%A3%00-%23%00-%E9%00-%DF%00-c", enc = "utf-16le") + + /* Do not test with utf-16 becauses it introduces BOM's in the middle of + * the encoded string, which is nonsensical. + */ + } +} From 811b2377855d42a3da00a3764d13266ef646c7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 14 Sep 2022 09:01:16 +0200 Subject: [PATCH 75/75] Version 1.11.0. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0d63dfa3ec..1da18493b9 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.11.0-SNAPSHOT", - binaryEmitted = "1.11-SNAPSHOT" + current = "1.11.0", + binaryEmitted = "1.11" ) /** Helper class to allow for testing of logic. */