From e85fdec78fe5e841471dcad84b9dd57fbd047572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 24 Apr 2026 22:29:47 +0200 Subject: [PATCH 1/7] Split huge string in generated Resources.scala --- build.sbt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 9f599c281..d436cb3bc 100644 --- a/build.sbt +++ b/build.sbt @@ -340,8 +340,17 @@ lazy val stdLibGenerator = Def.task { val virtuals = resources.get.map { file => val filename = file.relativeTo(baseDir).get - val content = IO.read(file).replace("$", "$$").replace("\"\"\"", "!!!MULTILINEMARKER!!!") - s"""loadIntoFile(raw\"\"\"$filename\"\"\", raw\"\"\"$content\"\"\")""" + val tripleQuote = "\"\"\"" + val content = IO.read(file) .replace("$", "$$") .replace(tripleQuote, "!!!MULTILINEMARKER!!!") + + // Split into ~64KB chunks to stay well under the compiler's name table limit + val chunkSize = 65536 + val chunks = content.grouped(chunkSize).toSeq + val chunksCode = chunks + .map(chunk => s"raw${tripleQuote}${chunk}${tripleQuote}") + .mkString(" +\n ") + + s"loadIntoFile(raw${tripleQuote}${filename}${tripleQuote}, $chunksCode)" } val scalaCode = From 5eabac1cd23a9f56268b6756b55c1c2e4fc2426f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 24 Apr 2026 22:34:21 +0200 Subject: [PATCH 2/7] Fix formatting --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d436cb3bc..c4363be6e 100644 --- a/build.sbt +++ b/build.sbt @@ -341,7 +341,7 @@ lazy val stdLibGenerator = Def.task { val virtuals = resources.get.map { file => val filename = file.relativeTo(baseDir).get val tripleQuote = "\"\"\"" - val content = IO.read(file) .replace("$", "$$") .replace(tripleQuote, "!!!MULTILINEMARKER!!!") + val content = IO.read(file).replace("$", "$$").replace(tripleQuote, "!!!MULTILINEMARKER!!!") // Split into ~64KB chunks to stay well under the compiler's name table limit val chunkSize = 65536 From f4a9cf1c88646f27d96941d2df685661128a3b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 24 Apr 2026 23:07:43 +0200 Subject: [PATCH 3/7] Encode stdlib into JSON on JS during build --- build.sbt | 122 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 51 deletions(-) diff --git a/build.sbt b/build.sbt index c4363be6e..44466c983 100644 --- a/build.sbt +++ b/build.sbt @@ -81,6 +81,9 @@ lazy val root = project.in(file("effekt")) Compile / run := (effekt.jvm / Compile / run).evaluated )) +val jsLinkTasks = Seq(fastLinkJS, fullLinkJS) +val jsLinkScopes = Seq(Compile, Test) + lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("effekt")) .settings( name := "effekt", @@ -272,7 +275,6 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e bench := benchmarks.measure.value ) .jsSettings( - assembleJS := { (Compile / clean).value (Compile / compile).value @@ -280,15 +282,32 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e val outputFile = (ThisBuild / baseDirectory).value / "out" / "effekt.js" IO.copyFile(jsFile, outputFile) }, - + scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, - + libraryDependencies += "com.lihaoyi" %%% "utest" % "0.8.2" % "test", - + testFrameworks += new TestFramework("utest.runner.Framework"), - - // include all resource files in the virtual file system - Compile / sourceGenerators += stdLibGenerator.taskValue + + Compile / sourceGenerators += stdLibGenerator.taskValue, + + // Ensure the JSON file is picked up as a managed resource, + // depending on stdLibGenerator so it is written first + Compile / resourceGenerators += Def.task { + stdLibGenerator.value // runs the generator, writes the JSON as a side effect + Seq((Compile / resourceManaged).value / "stdlib-resources.json") + }.taskValue, + + // Copy the JSON next to the linked output for every link step + for (scope <- jsLinkScopes; task <- jsLinkTasks) yield { + scope / task := { + val result = (scope / task).value + val json = (Compile / resourceManaged).value / "stdlib-resources.json" + val outDir = (scope / task / scalaJSLinkerOutputDirectory).value + IO.copyFile(json, outDir / "stdlib-resources.json") + result + } + } ) @@ -324,53 +343,54 @@ lazy val versionGenerator = Def.task { Seq(sourceFile) } -/** - * This generator is used by the JS version of our compiler to bundle the - * Effekt standard into the JS files and make them available in the virtual fs. - */ -lazy val stdLibGenerator = Def.task { - - val baseDir = (ThisBuild / baseDirectory).value / "libraries" - val resources = baseDir.glob("common" || "js") ** "*.*" - - val sourceDir = (Compile / sourceManaged).value - val sourceFile = sourceDir / "Resources.scala" - - if (!sourceFile.exists() || sourceFile.lastModified() < baseDir.lastModified()) { - - val virtuals = resources.get.map { file => - val filename = file.relativeTo(baseDir).get - val tripleQuote = "\"\"\"" - val content = IO.read(file).replace("$", "$$").replace(tripleQuote, "!!!MULTILINEMARKER!!!") - - // Split into ~64KB chunks to stay well under the compiler's name table limit - val chunkSize = 65536 - val chunks = content.grouped(chunkSize).toSeq - val chunksCode = chunks - .map(chunk => s"raw${tripleQuote}${chunk}${tripleQuote}") - .mkString(" +\n ") - - s"loadIntoFile(raw${tripleQuote}${filename}${tripleQuote}, $chunksCode)" - } - - val scalaCode = - s""" -package effekt.util -import effekt.util.paths._ - -object Resources { - - def loadIntoFile(filename: String, contents: String): Unit = - file(filename).write(contents.replace("!!!MULTILINEMARKER!!!", "\\"\\"\\"")) - - def load() = { -${virtuals.mkString("\n\n")} +// based on RFC 4267 directly: https://www.ietf.org/rfc/rfc4627.txt +def jsonEncode(s: String): String = { + val sb = new StringBuilder("\"") + s.foreach { + case '"' => sb.append("\\\"") + case '\\' => sb.append("\\\\") + case '\b' => sb.append("\\b") + case '\f' => sb.append("\\f") + case '\n' => sb.append("\\n") + case '\r' => sb.append("\\r") + case '\t' => sb.append("\\t") + case c if c < 0x20 => sb.append(f"\\u${c.toInt}%04x") + case c => sb.append(c) } + sb.append("\"").toString } -""" - IO.write(sourceFile, scalaCode) - } +lazy val stdLibGenerator = Def.task { + val baseDir = (ThisBuild / baseDirectory).value / "libraries" + val resources = (baseDir.glob("common" || "js") ** "*.*").get + + val jsonFile = (Compile / resourceManaged).value / "stdlib-resources.json" + val entries = resources.map { file => + val filename = file.relativeTo(baseDir).get.toString + val content = IO.read(file, IO.utf8) + s" ${jsonEncode(filename)}: ${jsonEncode(content)}" + }.mkString(",\n") + IO.write(jsonFile, s"{\n$entries\n}\n") + + // Thin Scala facade only to load 'stdlib-resources.json' + val sourceFile = (Compile / sourceManaged).value / "Resources.scala" + IO.write(sourceFile, + s"""package effekt.util + |import scala.scalajs.js + |import scala.scalajs.js.annotation.JSImport + |import effekt.util.paths._ + | + |@js.native + |@JSImport("./stdlib-resources.json", JSImport.Namespace) + |object StdlibData extends js.Object + | + |object Resources { + | def load(): Unit = StdlibData.asInstanceOf[js.Dictionary[String]].foreach { + | case (filename, content) => file(filename).write(content) + | } + |} + |""".stripMargin) Seq(sourceFile) } + From 1613b3072abab29d3d8e857bec409acda92dced6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 24 Apr 2026 23:11:54 +0200 Subject: [PATCH 4/7] Fix whitespace (again) --- build.sbt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 44466c983..c5bb17c53 100644 --- a/build.sbt +++ b/build.sbt @@ -275,6 +275,7 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e bench := benchmarks.measure.value ) .jsSettings( + assembleJS := { (Compile / clean).value (Compile / compile).value @@ -282,15 +283,15 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e val outputFile = (ThisBuild / baseDirectory).value / "out" / "effekt.js" IO.copyFile(jsFile, outputFile) }, - + scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, - + libraryDependencies += "com.lihaoyi" %%% "utest" % "0.8.2" % "test", - + testFrameworks += new TestFramework("utest.runner.Framework"), - + Compile / sourceGenerators += stdLibGenerator.taskValue, - + // Ensure the JSON file is picked up as a managed resource, // depending on stdLibGenerator so it is written first Compile / resourceGenerators += Def.task { From 9dc2fedd82bd1a6be558c5c983b69aaa88155958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 24 Apr 2026 23:15:14 +0200 Subject: [PATCH 5/7] Re-add and improve comments --- build.sbt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index c5bb17c53..c11c707e2 100644 --- a/build.sbt +++ b/build.sbt @@ -344,7 +344,7 @@ lazy val versionGenerator = Def.task { Seq(sourceFile) } -// based on RFC 4267 directly: https://www.ietf.org/rfc/rfc4627.txt +// escapes as per RFC 4267: https://www.ietf.org/rfc/rfc4627.txt def jsonEncode(s: String): String = { val sb = new StringBuilder("\"") s.foreach { @@ -361,10 +361,15 @@ def jsonEncode(s: String): String = { sb.append("\"").toString } +/** + * This generator is used by the JS version of our compiler to bundle the + * Effekt standard into the JS files (via JSON) and make them available in the virtual fs. + */ lazy val stdLibGenerator = Def.task { val baseDir = (ThisBuild / baseDirectory).value / "libraries" val resources = (baseDir.glob("common" || "js") ** "*.*").get + // 1. Generate the JSON file val jsonFile = (Compile / resourceManaged).value / "stdlib-resources.json" val entries = resources.map { file => val filename = file.relativeTo(baseDir).get.toString @@ -373,7 +378,7 @@ lazy val stdLibGenerator = Def.task { }.mkString(",\n") IO.write(jsonFile, s"{\n$entries\n}\n") - // Thin Scala facade only to load 'stdlib-resources.json' + // 2. Make a thin Scala facade to load the JSON val sourceFile = (Compile / sourceManaged).value / "Resources.scala" IO.write(sourceFile, s"""package effekt.util From 5f2c4af4697321e2500024e7ab1322ccb867b2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Tue, 28 Apr 2026 17:19:36 +0200 Subject: [PATCH 6/7] Use fullLinkJS instead of deprecated fullOptJS --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 8de5a3eb8..f734e0046 100644 --- a/build.sbt +++ b/build.sbt @@ -278,8 +278,8 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e assembleJS := { (Compile / clean).value - (Compile / compile).value - val jsFile = (Compile / fullOptJS).value.data + (Compile / fullLinkJS).value + val jsFile = (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value / "main.js" val outputFile = (ThisBuild / baseDirectory).value / "out" / "effekt.js" IO.copyFile(jsFile, outputFile) }, From 98bf7bb00beff204b1af85819fdca81381c3a1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Tue, 28 Apr 2026 17:30:51 +0200 Subject: [PATCH 7/7] Use 'assembleJS' in main CI --- .github/actions/run-effekt-tests/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/run-effekt-tests/action.yml b/.github/actions/run-effekt-tests/action.yml index 0c2a47351..4c174b45d 100644 --- a/.github/actions/run-effekt-tests/action.yml +++ b/.github/actions/run-effekt-tests/action.yml @@ -44,7 +44,7 @@ runs: - name: Assemble fully optimized js file if: ${{ inputs.full-test == 'true' && runner.os != 'Windows' }} - run: sbt effektJS/fullOptJS + run: sbt effektJS/assembleJS shell: bash - name: Try installing effekt binary