From ab1d5031605562f4fb1622cffc7665809cc6910f Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:18:40 +0800 Subject: [PATCH 01/22] Virtualize path and file system operations --- build.sbt | 9 +- hkmc2/js/src/main/scala/hkmc2/Main.scala | 16 ++ .../scala/hkmc2/io/PlatformFileSystem.scala | 34 +++ .../main/scala/hkmc2/io/PlatformPath.scala | 80 ++++++ .../scala/hkmc2/io/PlatformFileSystem.scala | 18 ++ .../main/scala/hkmc2/io/PlatformPath.scala | 68 +++++ .../test/scala/hkmc2/CompileTestRunner.scala | 2 + .../src/main/scala/hkmc2/Diagnostic.scala | 3 +- .../src/main/scala/hkmc2/MLsCompiler.scala | 30 ++- .../main/scala/hkmc2/codegen/Lowering.scala | 4 +- .../scala/hkmc2/codegen/js/JSBuilder.scala | 6 +- .../hkmc2/codegen/wasm/text/WatBuilder.scala | 2 +- .../src/main/scala/hkmc2/io/FileSystem.scala | 26 ++ .../scala/hkmc2/io/InMemoryFileSystem.scala | 26 ++ .../shared/src/main/scala/hkmc2/io/Path.scala | 61 +++++ .../scala/hkmc2/semantics/Elaborator.scala | 4 +- .../main/scala/hkmc2/semantics/Importer.scala | 29 ++- .../src/main/scala/hkmc2/semantics/Term.scala | 2 +- .../src/test/scala/hkmc2/BenchDiffMaker.scala | 4 +- .../test/scala/hkmc2/BenchTestRunner.scala | 1 + .../src/test/scala/hkmc2/BbmlDiffMaker.scala | 2 +- .../src/test/scala/hkmc2/DiffMaker.scala | 7 +- .../src/test/scala/hkmc2/DiffTestRunner.scala | 4 +- .../test/scala/hkmc2/JSBackendDiffMaker.scala | 2 +- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 22 +- .../src/test/scala/hkmc2/MainDiffMaker.scala | 3 +- .../src/test/scala/hkmc2/WasmDiffMaker.scala | 2 +- .../src/test/scala/hkmc2/Watcher.scala | 16 +- js/src/main/scala/Main.scala | 243 ------------------ js/src/main/scala/fansi/Str.scala | 14 - project/plugins.sbt | 2 +- 31 files changed, 417 insertions(+), 325 deletions(-) create mode 100644 hkmc2/js/src/main/scala/hkmc2/Main.scala create mode 100644 hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala create mode 100644 hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala create mode 100644 hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala create mode 100644 hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala create mode 100644 hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala create mode 100644 hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala create mode 100644 hkmc2/shared/src/main/scala/hkmc2/io/Path.scala delete mode 100644 js/src/main/scala/Main.scala delete mode 100644 js/src/main/scala/fansi/Str.scala diff --git a/build.sbt b/build.sbt index be6b89ba81..7c2c9d49a4 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ enablePlugins(ScalaJSPlugin) val scala3Version = "3.7.3" val directoryWatcherVersion = "0.18.0" -ThisBuild / scalaVersion := "2.13.14" +ThisBuild / scalaVersion := "2.13.18" ThisBuild / version := "0.1.0-SNAPSHOT" ThisBuild / organization := "hkust-taco.github.io" ThisBuild / organizationName := "HKUST-TACO" @@ -41,7 +41,8 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) libraryDependencies += "io.methvin" % "directory-watcher" % directoryWatcherVersion, libraryDependencies += "io.methvin" %% "directory-watcher-better-files" % directoryWatcherVersion, - libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", + libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.5.0", // Scala.js or Scala-Native + libraryDependencies += "com.lihaoyi" %%% "sourcecode" % "0.4.2", // Scala.js / Scala Native libraryDependencies += "com.lihaoyi" %% "os-lib" % "0.9.3", libraryDependencies += "org.scalactic" %%% "scalactic" % "3.2.18", @@ -56,6 +57,10 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) ) .jvmSettings( ) + .jsSettings( + scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }, + libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.2.0", + ) .dependsOn(core) lazy val hkmc2JVM = hkmc2.jvm diff --git a/hkmc2/js/src/main/scala/hkmc2/Main.scala b/hkmc2/js/src/main/scala/hkmc2/Main.scala new file mode 100644 index 0000000000..6f0bf29e3d --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/Main.scala @@ -0,0 +1,16 @@ +package hkmc2 + +import scala.util.Try +import scala.scalajs.js.annotation.* +import org.scalajs.dom +import org.scalajs.dom.document +import mlscript.utils._ +import mlscript.utils.shorthands._ +import scala.util.matching.Regex +import scala.scalajs.js +import scala.collection.immutable + +@JSExportTopLevel("MLscript") +object MLscript: + @JSExport + def compile(): String = "Hello, world!" diff --git a/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala new file mode 100644 index 0000000000..dc07e93db5 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala @@ -0,0 +1,34 @@ +package hkmc2 +package io + +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +import mlscript.utils._, shorthands._ + +/** + * Node.js fs module facade + */ +@js.native +@JSImport("fs", JSImport.Namespace) +private object NodeFs extends js.Object: + def readFileSync(path: String, encoding: String): String = js.native + def writeFileSync(path: String, data: String): Unit = js.native + def existsSync(path: String): Boolean = js.native + +/** + * JavaScript implementation of FileSystem using Node.js fs module + */ +private class JsFileSystem extends FileSystem: + def read(path: Path): String = + NodeFs.readFileSync(path.toString, "utf8") + + def write(path: Path, content: String): Unit = + NodeFs.writeFileSync(path.toString, content) + + def exists(path: Path): Bool = + NodeFs.existsSync(path.toString) + +// Platform-specific factory for FileSystem +private[io] object PlatformFileSystem: + def default: FileSystem = new JsFileSystem diff --git a/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala new file mode 100644 index 0000000000..f982005bbe --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala @@ -0,0 +1,80 @@ +package hkmc2 +package io + +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +import mlscript.utils._, shorthands._ + +@js.native +trait ParsedPath extends js.Object: + val base: String = js.native + val name: String = js.native + val ext: String = js.native + +/** + * Node.js path module facade + */ +@js.native +@JSImport("path", JSImport.Namespace) +object NodePath extends js.Object: + def sep: String = js.native + def parse(path: String): ParsedPath = js.native + def relative(from: String, to: String): String = js.native + def join(paths: String*): String = js.native + def isAbsolute(path: String): Boolean = js.native + def dirname(path: String): String = js.native + +/** + * JavaScript implementation of Path using Node.js path module + */ +private[io] class JsPath(val pathString: String) extends Path: + private lazy val parsed = NodePath.parse(pathString) + + override def toString: String = pathString + + def last: String = parsed.base + + def baseName: String = parsed.name + + def ext: String = + if parsed.ext.startsWith(".") then parsed.ext.substring(1) + else parsed.ext + + def up: Path = new JsPath(NodePath.dirname(pathString)) + + def /(relPath: RelPath): Path = + new JsPath(NodePath.join(pathString, relPath.toString)) + + def /(fragment: String): Path = + new JsPath(pathString + NodePath.sep + fragment) + + def relativeTo(base: Path): Opt[RelPath] = + try S(new JsRelPath(NodePath.relative(base.toString, pathString))) + catch case _: Exception => N + + def segments: Ls[String] = + pathString.split(NodePath.sep).toList.filter(_.nonEmpty) + + def isAbsolute: Bool = NodePath.isAbsolute(pathString) + +/** + * JavaScript implementation of RelPath using Node.js path module + */ +private[io] class JsRelPath(val pathString: String) extends RelPath: + override def toString: String = pathString + + def segments: Ls[String] = + pathString.split(NodePath.sep).toList.filter(_.nonEmpty) + + def /(other: RelPath): RelPath = + new JsRelPath(NodePath.join(pathString, other.toString)) + +/** + * Platform-specific factory for creating Path instances + */ +private[io] object PathFactory: + def fromString(str: String) = new JsPath(str) + def separator: String = NodePath.sep + def relPathFromString(str: String) = new JsRelPath(str) + def relPathUp = new JsRelPath("..") diff --git a/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala new file mode 100644 index 0000000000..f4885b4988 --- /dev/null +++ b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala @@ -0,0 +1,18 @@ +package hkmc2 +package io + +import mlscript.utils._, shorthands._ + +private[io] class JavaFileSystem extends FileSystem: + def read(path: Path): String = os.read(unwrap(path)) + + def write(path: Path, content: String): Unit = os.write.over(unwrap(path), content) + + def exists(path: Path): Bool = os.exists(unwrap(path)) + + private def unwrap(path: Path): os.Path = path match + case path: WrappedPath => path.underlying + case _ => lastWords(s"The given path is not compatible with the current platform (JVM).") + +object PlatformFileSystem: + val default: FileSystem = new JavaFileSystem diff --git a/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala new file mode 100644 index 0000000000..538527811a --- /dev/null +++ b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala @@ -0,0 +1,68 @@ +package hkmc2 +package io + +import mlscript.utils._, shorthands._ + +/** + * JVM implementation of [[Path]] that wraps [[os.Path]]. + */ +private[io] class WrappedPath(private[io] val underlying: os.Path) extends Path: + override def toString: String = underlying.toString + + def last: String = underlying.last + + def baseName: String = underlying.baseName + + def ext: String = underlying.ext + + def up: Path = new WrappedPath(underlying / os.up) + + def /(relPath: RelPath): Path = + new WrappedPath(underlying / relPath.asInstanceOf[WrappedRelPath].underlying) + + def /(fragment: Str): Path = + new WrappedPath(underlying / fragment) + + def relativeTo(base: Path): Opt[RelPath] = + try S(new WrappedRelPath(underlying.relativeTo(base.asInstanceOf[WrappedPath].underlying))) + catch case _: Exception => N + + def segments: Ls[String] = underlying.segments.toList + + def isAbsolute: Bool = underlying.startsWith(os.root) + +/** + * JVM implementation of [[RelPath]] that wraps [[os.RelPath]]. + */ +private[io] class WrappedRelPath(private[io] val underlying: os.RelPath) extends RelPath: + override def toString: String = underlying.toString + + def segments: Ls[String] = underlying.segments.toList + + def /(other: RelPath): RelPath = + new WrappedRelPath(underlying / other.asInstanceOf[WrappedRelPath].underlying) + +/** + * Platform-specific factory for creating Path instances + */ +private[io] object PathFactory: + def fromString(str: String) = new WrappedPath(os.Path(str)) + def separator: String = "/" + def relPathFromString(str: String) = new WrappedRelPath(os.RelPath(str)) + def relPathUp = new WrappedRelPath(os.up) + +/** + * JVM-specific utilities for Path conversion + */ +object PlatformPath: + /** Convert os.Path to io.Path */ + def fromOsPath(osPath: os.Path): Path = new WrappedPath(osPath) + + /** Convert os.RelPath to io.RelPath */ + def fromOsRelPath(osRelPath: os.RelPath): RelPath = new WrappedRelPath(osRelPath) + + /** Implicit conversion from os.Path to io.Path (import to use) */ + given Conversion[os.Path, Path] = fromOsPath + + /** Implicit conversion from os.RelPath to io.RelPath (import to use) */ + given Conversion[os.RelPath, RelPath] = fromOsRelPath diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala index 6587504b5b..a457d89f76 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala @@ -6,6 +6,7 @@ import org.scalatest.concurrent.{TimeLimitedTests, Signaler} import os.up import mlscript.utils._, shorthands._ +import io.PlatformPath.given class CompileTestRunner @@ -51,6 +52,7 @@ class CompileTestRunner // Stack safety relies on the fact that runtime uses while loops for resumption // and does not create extra stack depth. Hence we disable while loop rewriting here. given Config = Config.default.copy(rewriteWhileLoops = false) + given io.FileSystem = io.FileSystem.default val compiler = MLsCompiler( preludePath, diff --git a/hkmc2/shared/src/main/scala/hkmc2/Diagnostic.scala b/hkmc2/shared/src/main/scala/hkmc2/Diagnostic.scala index 3e0bb6111b..8da9d66daf 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Diagnostic.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Diagnostic.scala @@ -4,6 +4,7 @@ import scala.util.chaining._ import sourcecode.{Name, Line, FileName} import mlscript.utils._, shorthands._ +import hkmc2.io import Diagnostic._ @@ -93,6 +94,6 @@ object Loc: def apply(xs: IterableOnce[Located]): Opt[Loc] = xs.iterator.foldLeft(none[Loc])((acc, l) => acc.fold(l.toLoc)(_ ++ l.toLoc |> some)) -final case class Origin(fileName: os.Path, startLineNum: Int, fph: FastParseHelpers): +final case class Origin(fileName: io.Path, startLineNum: Int, fph: FastParseHelpers): override def toString = s"${fileName.last}:+$startLineNum" diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index de2597c1a7..311307f09b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -3,6 +3,7 @@ package hkmc2 import scala.collection.mutable import mlscript.utils.*, shorthands.* +import hkmc2.io import utils.* import hkmc2.semantics.MemberSymbol @@ -12,9 +13,9 @@ import hkmc2.syntax.Keyword.`override` import semantics.Elaborator.{Ctx, State} -class ParserSetup(file: os.Path, dbgParsing: Bool)(using Elaborator.State, Raise): - - val block = os.read(file) +class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State, raise: Raise, fs: io.FileSystem): + + val block = fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) @@ -37,10 +38,10 @@ class ParserSetup(file: os.Path, dbgParsing: Bool)(using Elaborator.State, Raise // * The weird type of `mkOutput` is to allow wrapping the reporting of diagnostics in synchronized blocks -class MLsCompiler(preludeFile: os.Path, mkOutput: ((Str => Unit) => Unit) => Unit)(using Config): - - val runtimeFile: os.Path = preludeFile/os.up/os.up/os.up/"mlscript-compile"/"Runtime.mjs" - val termFile: os.Path = preludeFile/os.up/os.up/os.up/"mlscript-compile"/"Term.mjs" +class MLsCompiler(preludeFile: io.Path, mkOutput: ((Str => Unit) => Unit) => Unit)(using cfg: Config, fs: io.FileSystem): + + val runtimeFile: io.Path = preludeFile.up.up.up / io.RelPath("mlscript-compile/Runtime.mjs") + val termFile: io.Path = preludeFile.up.up.up / io.RelPath("mlscript-compile/Term.mjs") val report = ReportFormatter: outputConsumer => @@ -58,13 +59,14 @@ class MLsCompiler(preludeFile: os.Path, mkOutput: ((Str => Unit) => Unit) => Uni var dbgParsing = false - def compileModule(file: os.Path): Unit = - - val wd = file / os.up - + def compileModule(file: io.Path): Unit = + + val wd = file.up + given raise: Raise = d => mkOutput: - _(fansi.Color.LightRed(s"/!!!\\ Error in ${file.relativeTo(wd/os.up)} /!!!\\").toString) + val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) + _(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) report(0, d :: Nil, showRelativeLineNums = false) given Elaborator.State = new Elaborator.State @@ -107,8 +109,8 @@ class MLsCompiler(preludeFile: os.Path, mkOutput: ((Str => Unit) => Unit) => Uni val je = nestedScp.givenIn: jsb.program(le, exportedSymbol, wd) val jsStr = je.stripBreaks.mkString(100) - val out = file / os.up / (file.baseName + ".mjs") - os.write.over(out, jsStr) + val out = file.up / io.RelPath(file.baseName + ".mjs") + fs.write(out, jsStr) end MLsCompiler diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index d4922ec4c2..3a17c51e73 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -780,9 +780,9 @@ class Lowering()(using Config, TL, Raise, State, Ctx): (t.toLoc, sym.toLoc) match case (S(Loc(_, _, Origin(base, _, _))), S(Loc(_, _, Origin(filename, _, _)))) => setupSymbol(sym): r1 => val l1, l2 = new TempSymbol(N) - val basePath = base / os.up + val basePath = base.up val targetPath = filename - val relPath = targetPath.relativeTo(basePath).toString + val relPath = targetPath.relativeTo(basePath).map(_.toString).getOrElse(targetPath.toString) Assign(l1, r1, setupTerm("CSRef", Value.Ref(l1) :: setupFilename :: Value.Lit(syntax.Tree.StrLit(relPath)) :: Nil)(r2 => Assign(l2, r2, setupTerm("Sel", Value.Ref(l2) :: Value.Lit(syntax.Tree.StrLit(name.name)) :: Nil)(k)) )) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala index b5fb6e9176..fb6428f4bc 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala @@ -553,14 +553,14 @@ class JSBuilder(using TL, State, Ctx) extends CodeBuilder: case _ => blk.subBlocks.foreach(go) go(p.main) - def program(p: Program, exprt: Opt[BlockMemberSymbol], wd: os.Path)(using Raise, Scope): Document = + def program(p: Program, exprt: Opt[BlockMemberSymbol], wd: io.Path)(using Raise, Scope): Document = scope.allocateName(State.definitionMetadataSymbol) scope.allocateName(State.prettyPrintSymbol) doc"""const ${getVar(State.definitionMetadataSymbol, N)} = globalThis.Symbol.for("mlscript.definitionMetadata");""" :/: doc"""const ${getVar(State.prettyPrintSymbol, N)} = globalThis.Symbol.for("mlscript.prettyPrint");""" :/: programBody(p, exprt, wd) - def programBody(p: Program, exprt: Opt[BlockMemberSymbol], wd: os.Path)(using Raise, Scope): Document = + def programBody(p: Program, exprt: Opt[BlockMemberSymbol], wd: io.Path)(using Raise, Scope): Document = reserveNames(p) // Allocate names for imported modules. p.imports.foreach: i => @@ -569,7 +569,7 @@ class JSBuilder(using TL, State, Ctx) extends CodeBuilder: val imps = p.imports.map: i => val path = i._2 val relPath = if path.startsWith("/") - then "./" + os.Path(path).relativeTo(wd).toString + then "./" + io.Path(path).relativeTo(wd).map(_.toString).getOrElse(path) else path doc"""import ${getVar(i._1, N)} from "${relPath}";""" imps.mkDocument(doc" # ") :/: block(p.main, endSemi = false).stripBreaks :: ( diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index f115678862..77dc50a266 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -726,7 +726,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: ) end returningTerm - def program(p: Program, exprt: Opt[BlockMemberSymbol], wd: os.Path)(using + def program(p: Program, exprt: Opt[BlockMemberSymbol], wd: io.Path)(using Raise, Scope ): (Document, Str) = diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala b/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala new file mode 100644 index 0000000000..0c9ae990bb --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala @@ -0,0 +1,26 @@ +package hkmc2 +package io + +import mlscript.utils._, shorthands._ + +/** + * Abstract file system operations. + * + * These are file system operations that can be directly called by the compiler. + * More high-level file system operations, such as getting all files under a + * folder or recursively walking through a specified path, should be handled by + * the caller of the compiler. + */ +trait FileSystem: + /** Read entire file as string. */ + def read(path: Path): String + + /** Write string to file, overwriting if exists. */ + def write(path: Path, content: String): Unit + + /** Check if a file exists at the given path. */ + def exists(path: Path): Bool + +object FileSystem: + /** Get the platform default file system by delegating to the platform. */ + def default: FileSystem = PlatformFileSystem.default diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala b/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala new file mode 100644 index 0000000000..fd9e316316 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala @@ -0,0 +1,26 @@ +package hkmc2.io + +import mlscript.utils._, shorthands._ +import collection.mutable.Map as MutMap + +/** + * In-memory file system for testing and web compiler. Stores files as a map + * from path strings to content strings. Note that separators are not normalized. + */ +class InMemoryFileSystem(initialFiles: Map[String, String]) extends FileSystem: + private val files: MutMap[String, String] = MutMap.from(initialFiles) + + def read(path: Path): String = + files.getOrElse(path.toString, throw new java.io.FileNotFoundException(path.toString)) + + def write(path: Path, content: String): Unit = + files(path.toString) = content + + def exists(path: Path): Bool = files.contains(path.toString) + + /** Add a file to the virtual file system (for testing) */ + def addFile(path: String, content: String): Unit = + files(path) = content + + /** Get all files (for debugging) */ + def allFiles: Map[String, String] = files.toMap diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala new file mode 100644 index 0000000000..fe9a693251 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala @@ -0,0 +1,61 @@ +package hkmc2 +package io + +import mlscript.utils._, shorthands._ + +/** + * Cross-platform path abstraction. + * Represents a file system path without performing any I/O. + */ +abstract class Path: + /** Convert to platform-specific string representation */ + def toString: String + + /** Get the last segment of the path (file or directory name) */ + def last: String + + /** Get the base name without extension */ + def baseName: String + + /** Get the file extension (without dot) */ + def ext: String + + /** Navigate to parent directory */ + def up: Path + + /** Join with a relative path */ + def /(relPath: RelPath): Path + + /** Join with a single path fragment */ + def /(fragment: Str): Path + + /** Calculate relative path from this to target */ + def relativeTo(base: Path): Opt[RelPath] + + /** Get segments as a list */ + def segments: Ls[String] + + /** Check if this is an absolute path */ + def isAbsolute: Bool + +object Path: + /** Create path from string - delegates to platform-specific implementation */ + def apply(str: String): Path = PathFactory.fromString(str) + + /** Platform-specific path separator */ + def separator: String = PathFactory.separator + +/** + * Represents a relative path + */ +abstract class RelPath: + def toString: String + def segments: Ls[String] + def /(other: RelPath): RelPath + +object RelPath: + /** Create relative path from string - delegates to platform-specific implementation */ + def apply(str: String): RelPath = PathFactory.relPathFromString(str) + + /** Represents parent directory (..) */ + val up: RelPath = PathFactory.relPathUp diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 9b4c220046..0ddaa08fdc 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -302,8 +302,8 @@ end Elaborator import Elaborator.* -class Elaborator(val tl: TraceLogger, val wd: os.Path, val prelude: Ctx) -(using val raise: Raise, val state: State) +class Elaborator(val tl: TraceLogger, val wd: io.Path, val prelude: Ctx) +(using val raise: Raise, val state: State, val fs: io.FileSystem) extends Importer with ucs.SplitElaborator: import tl.* diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index 0f7e98a0c9..a96bb30946 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -6,6 +6,7 @@ import scala.annotation.tailrec import mlscript.utils.*, shorthands.* import hkmc2.Message.MessageContext +import hkmc2.io import utils.TraceLogger import Elaborator.* @@ -14,15 +15,15 @@ import hkmc2.syntax.LetBind class Importer: self: Elaborator => import tl.* - - def importPath(path: Str)(using Config): Import = + + def importPath(path: Str)(using cfg: Config, fs: io.FileSystem): Import = // log(s"pwd: ${os.pwd}") // log(s"wd: ${wd}") val file = if path.startsWith("/") - then os.Path(path) - else wd / os.RelPath(path) + then io.Path(path) + else wd / io.RelPath(path) val nme = file.baseName val id = new syntax.Tree.Ident(nme) // TODO loc @@ -41,15 +42,15 @@ class Importer: Import(sym, file.toString, file) case "mls" => - - val block = os.read(file) + + val block = fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) - + val sym = tl.trace(s">>> Importing $file"): - + // TODO add parser option to omit internal impls - + val lexer = new syntax.Lexer(origin, dbg = tl.doTrace) val tokens = lexer.bracketedTokens val rules = syntax.ParseRules() @@ -59,16 +60,16 @@ class Importer: if dbg then tl.log(msg) val res = p.parseAll(p.block(allowNewlines = true)) val resBlk = new syntax.Tree.Block(res) - + given Elaborator.Ctx = prelude.copy(mode = Mode.Light).nestLocal("prelude") - val elab = Elaborator(tl, file / os.up, prelude) + val elab = Elaborator(tl, file.up, prelude) elab.importFrom(resBlk) - + resBlk.definedSymbols.find(_._1 === nme) match case Some(nme -> sym) => sym case None => lastWords(s"File $file does not define a symbol named $nme") - - val jsFile = file / os.up / (file.baseName + ".mjs") + + val jsFile = file.up / io.RelPath(file.baseName + ".mjs") Import(sym, jsFile.toString, jsFile) case _ => diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 3c18cae966..bffc1091af 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -744,7 +744,7 @@ case class ObjBody(blk: Term.Blk): /** Note that the `file` Path may not represent a real file; eg when importing "fs". */ -case class Import(sym: Symbol, str: Str, file: os.Path) extends Statement +case class Import(sym: Symbol, str: Str, file: io.Path) extends Statement sealed abstract class Declaration: diff --git a/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala b/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala index aaeded43bb..5bebcc5ca3 100644 --- a/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala +++ b/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala @@ -4,8 +4,10 @@ import mlscript.utils._, shorthands._ import hkmc2.syntax.Tree import hkmc2.syntax.Keyword -class BenchDiffMaker(val rootPath: Str, val file: os.Path, val preludeFile: os.Path, val predefFile: os.Path, val relativeName: Str) +class BenchDiffMaker(val rootPath: Str, val file: io.Path, val preludeFile: io.Path, val predefFile: io.Path, val relativeName: Str) extends LlirDiffMaker: + + override def fs = io.FileSystem.default override def processTerm(blk: semantics.Term.Blk, inImport: Bool)(using Config, Raise): Unit = super.processTerm(blk, inImport) diff --git a/hkmc2Benchmarks/src/test/scala/hkmc2/BenchTestRunner.scala b/hkmc2Benchmarks/src/test/scala/hkmc2/BenchTestRunner.scala index 0d6b99733f..4ce757c429 100644 --- a/hkmc2Benchmarks/src/test/scala/hkmc2/BenchTestRunner.scala +++ b/hkmc2Benchmarks/src/test/scala/hkmc2/BenchTestRunner.scala @@ -5,6 +5,7 @@ import org.scalatest.time._ import mlscript.utils._ import os.Path +import io.PlatformPath.given object BenchTestState extends DiffTestRunner.State: diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala index 3ffa0837ab..754ef23695 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala @@ -9,7 +9,7 @@ import utils.Scope abstract class BbmlDiffMaker extends JSBackendDiffMaker: - val bbPreludeFile = file / os.up / os.RelPath("bbPrelude.mls") + val bbPreludeFile = file.up / "bbPrelude.mls" val bbmlOpt = new NullaryCommand("bbml"): override def onSet(): Unit = diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala index f18c049279..93cbc932d5 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala @@ -27,8 +27,9 @@ class Outputter(val out: java.io.PrintWriter): abstract class DiffMaker: + protected given fs: io.FileSystem - val file: os.Path + val file: io.Path val relativeName: Str def processOrigin(origin: Origin)(using Raise): Unit @@ -135,7 +136,7 @@ abstract class DiffMaker: val fileName = file.last - val fileContents = os.read(file) + val fileContents = fs.read(file) val allLines = fileContents.splitSane('\n').toList val strw = new java.io.StringWriter val out = new java.io.PrintWriter(strw) @@ -351,7 +352,7 @@ abstract class DiffMaker: val result = strw.toString if result =/= fileContents then println(s"Updating $file...") - os.write.over(file, result) + fs.write(file, result) // * Called after the very first command block // * and every time a further command block with `:init` finishes diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala index fad23ec808..b5ddaf4401 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala @@ -6,6 +6,7 @@ import org.scalatest.concurrent.{TimeLimitedTests, Signaler} import os.up import mlscript.utils._, shorthands._ +import io.PlatformPath.given, io.FileSystem // * Note: we used to use: @@ -118,7 +119,8 @@ class DiffTestRunnerBase(state: DiffTestRunner.State) predefPath: os.Path, relativeName: String ): DiffMaker = - new MainDiffMaker(workingDir.toString, file, preludePath, predefPath, relativeName) + new MainDiffMaker(workingDir.toString, file, preludePath, predefPath, relativeName): + override def fs = FileSystem.default diffTestFiles.foreach: file => diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index d115478cde..570f529d22 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -50,7 +50,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: hostCreated = true given TL = replTL val h = ReplHost(rootPath) - def importRuntimeModule(name: Str, file: os.Path) = + def importRuntimeModule(name: Str, file: io.Path) = h.execute(s"const $name = (await import(\"${file}\")).default;") match case ReplHost.Result(msg) => if msg.startsWith("Uncaught") then output(s"Failed to load $name: $msg") diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index f527cb10d8..95dcfbb518 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -16,14 +16,14 @@ abstract class MLsDiffMaker extends DiffMaker: val bbmlOpt: Command[?] val rootPath: Str // * Absolute path to the root of the project - val preludeFile: os.Path // * Contains declarations of JS builtins - val predefFile: os.Path // * Contains MLscript standard library definitions - val runtimeFile: os.Path = predefFile/os.up/"Runtime.mjs" // * Contains MLscript runtime definitions - val termFile: os.Path = predefFile/os.up/"Term.mjs" // * Contains MLscript runtime term definitions - val blockFile: os.Path = predefFile/os.up/"Block.mjs" // * Contains MLscript runtime block definitions - val shapeFile: os.Path = predefFile/os.up/"Shape.mjs" // * Contains MLscript runtime shape definitions + val preludeFile: io.Path // * Contains declarations of JS builtins + val predefFile: io.Path // * Contains MLscript standard library definitions + val runtimeFile: io.Path = predefFile.up / "Runtime.mjs" // * Contains MLscript runtime definitions + val termFile: io.Path = predefFile.up / "Term.mjs" // * Contains MLscript runtime term definitions + val blockFile: io.Path = predefFile.up / "Block.mjs" // * Contains MLscript runtime block definitions + val shapeFile: io.Path = predefFile.up / "Shape.mjs" // * Contains MLscript runtime shape definitions - val wd = file / os.up + val wd = file.up class DebugTreeCommand(name: Str) extends Command[Product => Str](name)( line => if line.contains("loc") then @@ -105,7 +105,7 @@ abstract class MLsDiffMaker extends DiffMaker: val importCmd = Command("import"): ln => given Config = mkConfig - importFile(file / os.up / os.RelPath(ln.trim), verbose = silent.isUnset) + importFile(file.up / io.RelPath(ln.trim), verbose = silent.isUnset) val showUCS = Command("ucs"): ln => ln.split(" ").iterator.map(x => "ucs:" + x.trim).toSet @@ -171,14 +171,14 @@ abstract class MLsDiffMaker extends DiffMaker: super.init() - def importFile(file: os.Path, verbose: Bool)(using Config): Unit = + def importFile(file: io.Path, verbose: Bool)(using Config): Unit = // val raise: Raise = throw _ given raise: Raise = d => output(s"Error: $d") () - val block = os.read(file) + val block = fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) @@ -253,7 +253,7 @@ abstract class MLsDiffMaker extends DiffMaker: private var blockNum = 0 def processTrees(trees: Ls[syntax.Tree])(using Config, Raise): Unit = - val elab = Elaborator(etl, file / os.up, prelude) + val elab = Elaborator(etl, file.up, prelude) // val blockSymbol = // semantics.TopLevelSymbol("block#"+blockNum) blockNum += 1 diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MainDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MainDiffMaker.scala index 41e1587c49..f0de1d2a63 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MainDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MainDiffMaker.scala @@ -3,12 +3,11 @@ package hkmc2 import org.scalatest.{funsuite, ParallelTestExecution} import org.scalatest.time._ import org.scalatest.concurrent.{TimeLimitedTests, Signaler} -import os.up import mlscript.utils._, shorthands._ -class MainDiffMaker(val rootPath: Str, val file: os.Path, val preludeFile: os.Path, val predefFile: os.Path, val relativeName: Str) +abstract class MainDiffMaker(val rootPath: Str, val file: io.Path, val preludeFile: io.Path, val predefFile: io.Path, val relativeName: Str) extends WasmDiffMaker diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala index 27a05487f2..01ae50f78b 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala @@ -28,7 +28,7 @@ abstract class WasmDiffMaker extends LlirDiffMaker: private val baseScp: utils.Scope = utils.Scope.empty - final lazy val wasmSuppFile: os.Path = predefFile / os.up / "Wasm.mjs" + final lazy val wasmSuppFile: io.Path = predefFile.up / "Wasm.mjs" final lazy val wasmSuppNme = baseScp.allocateName(Elaborator.State.wasmSymbol) final lazy val loadWasm: Unit = host.execute( diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index fa5a08c698..a01348951a 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -6,11 +6,13 @@ import scala.jdk.CollectionConverters.* import mlscript.utils.*, shorthands.* import better.files.* -import io.methvin.better.files.* -import io.methvin.watcher.{DirectoryWatcher, PathUtils} -import io.methvin.watcher.hashing.{FileHash, FileHasher} +import _root_.io.methvin.better.files.* +import _root_.io.methvin.watcher.{DirectoryWatcher, PathUtils} +import _root_.io.methvin.watcher.{DirectoryChangeEvent, DirectoryChangeListener} +import _root_.io.methvin.watcher.hashing.{FileHash, FileHasher} import java.time.LocalDateTime import java.time.temporal._ +import io.FileSystem, io.PlatformPath.given // Note: when SBT's `fork` is set to `false`, the path should be `File("hkmc2/")` instead... // * Only the first path can contain tests. The other paths are only watched for source changes. @@ -31,8 +33,8 @@ class Watcher(dirs: Ls[File]): .logger(org.slf4j.helpers.NOPLogger.NOP_LOGGER) .paths(dirs.map(_.toJava.toPath).asJava) .fileHashing(false) // so that simple save events trigger processing eve if there's no file change - .listener(new io.methvin.watcher.DirectoryChangeListener { - def onEvent(event: io.methvin.watcher.DirectoryChangeEvent): Unit = try + .listener(new DirectoryChangeListener { + def onEvent(event: DirectoryChangeEvent): Unit = try // println(event) val hash = PathUtils.hash(fileHasher, event.path) val file = File(event.path) @@ -60,7 +62,7 @@ class Watcher(dirs: Ls[File]): val et = event.eventType val count = event.count et match - case io.methvin.watcher.DirectoryChangeEvent.EventType.OVERFLOW => ??? + case DirectoryChangeEvent.EventType.OVERFLOW => ??? case _ => et.getWatchEventKind.asInstanceOf[WatchEvent.Kind[Path]] match case StandardWatchEventKinds.ENTRY_CREATE => onCreate(file, count) @@ -96,9 +98,11 @@ class Watcher(dirs: Ls[File]): if isModuleFile then given Config = Config.default + given FileSystem = FileSystem.default MLsCompiler(preludePath, outputConsumer => outputConsumer(System.out.println)).compileModule(path) else val dm = new MainDiffMaker(rootPath.toString, path, preludePath, predefPath, relativeName): + override def fs = FileSystem.default override def unhandled(blockLineNum: Int, exc: Throwable): Unit = exc.printStackTrace() super.unhandled(blockLineNum, exc) diff --git a/js/src/main/scala/Main.scala b/js/src/main/scala/Main.scala deleted file mode 100644 index e6c59c21fc..0000000000 --- a/js/src/main/scala/Main.scala +++ /dev/null @@ -1,243 +0,0 @@ -import scala.util.Try -import scala.scalajs.js.annotation.JSExportTopLevel -import org.scalajs.dom -import org.scalajs.dom.document -import org.scalajs.dom.raw.{Event, TextEvent, UIEvent, HTMLTextAreaElement} -import mlscript.utils._ -import mlscript._ -import mlscript.utils.shorthands._ -import scala.util.matching.Regex -import scala.scalajs.js -import scala.collection.immutable - -object Main { - def main(args: Array[String]): Unit = { - val source = document.querySelector("#mlscript-input") - update(source.textContent) - source.addEventListener("input", typecheck) - } - @JSExportTopLevel("typecheck") - def typecheck(e: dom.UIEvent): Unit = { - e.target match { - case elt: dom.HTMLTextAreaElement => - update(elt.value) - } - } - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - def update(str: String): Unit = { - // println(s"Input: $str") - val target = document.querySelector("#mlscript-output") - - - def underline(fragment: Str): Str = - s"$fragment" - - var totalTypeErrors = 0 - var totalWarnings = 0 - var outputMarker = "" - val blockLineNum = 0 - val showRelativeLineNums = false - - def report(diag: Diagnostic): Str = { - var sb = new collection.mutable.StringBuilder - def output(s: Str): Unit = { - sb ++= outputMarker - sb ++= s - sb ++= htmlLineBreak - () - } - val sctx = Message.mkCtx(diag.allMsgs.iterator.map(_._1), newDefs=true, "?") - val headStr = diag match { - case ErrorReport(msg, loco, src) => - totalTypeErrors += 1 - s"╔══ [ERROR] " - case WarningReport(msg, loco, src) => - totalWarnings += 1 - s"╔══ [WARNING] " - } - val lastMsgNum = diag.allMsgs.size - 1 - var globalLineNum = - blockLineNum // solely used for reporting useful test failure messages - diag.allMsgs.zipWithIndex.foreach { case ((msg, loco), msgNum) => - val isLast = msgNum =:= lastMsgNum - val msgStr = msg.showIn(sctx) - if (msgNum =:= 0) - output(headStr + msgStr) - else - output(s"${if (isLast && loco.isEmpty) "╙──" else "╟──"} ${msgStr}") - if (loco.isEmpty && diag.allMsgs.size =:= 1) output("╙──") - loco.foreach { loc => - val (startLineNum, startLineStr, startLineCol) = - loc.origin.fph.getLineColAt(loc.spanStart) - if (globalLineNum =:= 0) globalLineNum += startLineNum - 1 - val (endLineNum, endLineStr, endLineCol) = - loc.origin.fph.getLineColAt(loc.spanEnd) - var l = startLineNum - var c = startLineCol // c starts from 1 - while (l <= endLineNum) { - val globalLineNum = loc.origin.startLineNum + l - 1 - val relativeLineNum = globalLineNum - blockLineNum + 1 - val shownLineNum = - if (showRelativeLineNums && relativeLineNum > 0) - s"l.+$relativeLineNum" - else "l." + globalLineNum - val prepre = "║ " - val pre = s"$shownLineNum: " // Looks like l.\d+ - val curLine = loc.origin.fph.lines(l - 1) - val lastCol = - if (l =:= endLineNum) endLineCol else curLine.length + 1 - val front = curLine.slice(0, c - 1) - val middle = underline(curLine.slice(c - 1, lastCol - 1)) - val back = curLine.slice(lastCol - 1, curLine.length) - output(s"$prepre$pre\t$front$middle$back") - c = 1 - l += 1 - if (isLast) output("╙──") - } - } - } - if (diag.allMsgs.isEmpty) output("╙──") - sb.toString - } - - val tryRes = try { - import fastparse._ - import fastparse.Parsed.{Success, Failure} - import mlscript.{NewParser, ErrorReport, Origin} - val lines = str.splitSane('\n').toIndexedSeq - val processedBlock = lines.mkString - val fph = new mlscript.FastParseHelpers(str, lines) - val origin = Origin("", 1, fph) - val lexer = new NewLexer(origin, throw _, dbg = false) - val tokens = lexer.bracketedTokens - val parser = new NewParser(origin, tokens, newDefs = true, throw _, dbg = false, N) { - def doPrintDbg(msg: => Str): Unit = if (dbg) println(msg) - } - parser.parseAll(parser.typingUnit) match { - case tu => - val pgrm = Pgrm(tu.entities) - println(s"Parsed: $pgrm") - - val typer = new mlscript.Typer( - dbg = false, - verbose = false, - explainErrors = false, - newDefs = true, - ) - - import typer._ - - implicit val raise: Raise = throw _ - implicit var ctx: Ctx = Ctx.init - implicit val extrCtx: Opt[typer.ExtrCtx] = N - - val vars: Map[Str, typer.SimpleType] = Map.empty - val tpd = typer.typeTypingUnit(tu, N)(ctx.nest, raise, vars) - - object SimplifyPipeline extends typer.SimplifyPipeline { - def debugOutput(msg: => Str): Unit = - // if (mode.dbgSimplif) output(msg) - println(msg) - } - val sim = SimplifyPipeline(tpd, S(true))(ctx) - - val exp = typer.expandType(sim)(ctx) - - val expStr = exp.showIn(0)(ShowCtx.mk(exp :: Nil, newDefs = true)).stripSuffix("\n") - .replaceAll(" ", "  ") - .replaceAll("\n", "
") - - // TODO format HTML better - val typingStr = """
- | - | - | - |""".stripMargin + - s""" - | ${s""} - | - |""".stripMargin - - val backend = new JSWebBackend() - val (lines, resNames) = backend(pgrm) - val code = lines.mkString("\n") - - // TODO: add a toggle button to show js code - // val jsStr = ("\n\n=====================JavaScript Code=====================\n" + code) - // .stripSuffix("\n") - // .replaceAll(" ", "  ") - // .replaceAll("\n", "
") - - val exe = executeCode(code) match { - case Left(err) => err - case Right(lines) => generateResultTable(resNames.zip(lines)) - } - - val resStr = (""" - | - | - |""".stripMargin + exe + "

Typing Results:

${expStr}

Execution Results:

") - - typingStr + resStr - } - } catch { - // case err: ErrorReport => - case err: Diagnostic => - report(err) - case err: Throwable => - s""" - - Unexpected error: ${err}${ - err.printStackTrace - // err.getStackTrace().map(s"$htmlLineBreak$htmlWhiteSpace$htmlWhiteSpace at " + _).mkString - "" - }""" - } - - target.innerHTML = tryRes - } - - // Execute the generated code. - // We extract this function because there is some boilerplate code. - // It returns a tuple of three items: - // 1. results of definitions; - // 2. results of expressions; - // 3. error message (if has). - @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - private def executeCode(code: Str): Either[Str, Ls[Str]] = { - try { - R(js.eval(code).asInstanceOf[js.Array[Str]].toList) - } catch { - case e: Throwable => - val errorBuilder = new StringBuilder() - errorBuilder ++= "Runtime error occurred:" - errorBuilder ++= htmlLineBreak + e.getMessage - errorBuilder ++= htmlLineBreak - errorBuilder ++= htmlLineBreak - L(errorBuilder.toString) - } - } - - private def generateResultTable(res: Ls[(Str, Str)]): Str = { - val htmlBuilder = new StringBuilder - htmlBuilder ++= """ - | Name - | Value - | - |""".stripMargin - - res.foreach(value => { - htmlBuilder ++= s""" - | ${value._1.replaceAll(" ", "  ").replaceAll("\n", "
")} - | ${s"${value._2.replaceAll(" ", "  ").replaceAll("\n", "
")}"} - | - |""".stripMargin - }) - - htmlBuilder.toString - } - - private val htmlLineBreak = "
" - private val htmlWhiteSpace = " " -} - diff --git a/js/src/main/scala/fansi/Str.scala b/js/src/main/scala/fansi/Str.scala deleted file mode 100644 index 58c9b91823..0000000000 --- a/js/src/main/scala/fansi/Str.scala +++ /dev/null @@ -1,14 +0,0 @@ -// Temporary shims since fansi doesn't seem to be released for this Scala version yet - -package fansi - -import scala.language.implicitConversions - -class Str(underlying: CharSequence) { - def plainText(): String = s"$underlying" - override def toString(): String = s"$underlying" -} -object Str { - implicit def implicitApply(x: CharSequence): Str = new Str(x) - def join(args: Str*): Str = args.foldLeft("")(_ ++ _.plainText()) -} diff --git a/project/plugins.sbt b/project/plugins.sbt index 374a7d0076..ebf7a321b8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,3 @@ addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.4.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") From ca53316497dd18b4d37cc54434bee341f808af1a Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:17:00 +0800 Subject: [PATCH 02/22] Test calling the complete compiler from JavaScript --- build.sbt | 41 ++++++++++++++++- hkmc2/js/src/main/scala/hkmc2/MLscript.scala | 45 +++++++++++++++++++ hkmc2/js/src/main/scala/hkmc2/Main.scala | 16 ------- .../scala/hkmc2/io/PlatformFileSystem.scala | 7 ++- .../main/scala/hkmc2/io/PlatformPath.scala | 20 ++++----- .../src/main/scala/hkmc2/io/FileSystem.scala | 3 ++ .../scala/hkmc2/io/InMemoryFileSystem.scala | 2 +- test.mjs | 13 ++++++ 8 files changed, 115 insertions(+), 32 deletions(-) create mode 100644 hkmc2/js/src/main/scala/hkmc2/MLscript.scala delete mode 100644 hkmc2/js/src/main/scala/hkmc2/Main.scala create mode 100644 test.mjs diff --git a/build.sbt b/build.sbt index 7c2c9d49a4..dfe7c9da02 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,5 @@ import Wart._ +import org.scalajs.linker.interface.OutputPatterns enablePlugins(ScalaJSPlugin) @@ -58,8 +59,46 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) .jvmSettings( ) .jsSettings( - scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }, + scalaJSLinkerConfig ~= { + _.withModuleKind(ModuleKind.ESModule) + .withOutputPatterns(OutputPatterns.fromJSFile("MLscript.mjs")) + }, libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.2.0", + // We directly read the necessary MLscript files from the test folders to + // avoid manually embedding the source files in the `WebImporter` class. + Compile / sourceGenerators += Def.task { + def escape(content: String): String = content.iterator.flatMap { + case '\b' => "\\b" case '\t' => "\\t" case '\n' => "\\n" + case '\r' => "\\r" case '\f' => "\\f" case '"' => "\\\"" + case '\\' => "\\\\" case c if c.isControl => f"\\u${c.toInt}%04x" + case c => c.toString + }.mkString("\"", "", "\"") + // Note: baseDirectory.value = ups-web-demo/hkmc2/js + val testFolder = baseDirectory.value / ".." / "shared" / "src" / "test" + val compileFolder = testFolder / "mlscript-compile" + val preludeFile = IO.read(testFolder / "mlscript" / "decls" / "Prelude.mls") + val stdFiles = List("Predef", "Runtime", "Rendering", "Stack", "Iter", "Option") + .iterator + .map { fileName => + (compileFolder / s"$fileName.mls", compileFolder / s"$fileName.mjs") + }.map { case (mlsPath, mjsPath) => + (mlsPath.getName(), IO.read(mlsPath), IO.read(mjsPath)) + }.toList + val outFile = (Compile / sourceManaged).value / "generated" / "MLscript.scala" + IO.write( + outFile, + s"""|package hkmc2.generated + |import collection.mutable.Map as MutMap + |object MLscript: + | val preludeFile = ${escape(preludeFile)} + | val sourceFiles: MutMap[String, (String, String)] = MutMap.empty + |""".stripMargin + + (stdFiles.iterator.map { case (fileName, mlsContent, mjsContent) => + s" sourceFiles += (${escape(s"/std/$fileName")} -> (${escape(mlsContent)}, ${escape(mjsContent)}))" + }.mkString("\n")) + "\n" + ) + Seq(outFile) + }.taskValue ) .dependsOn(core) diff --git a/hkmc2/js/src/main/scala/hkmc2/MLscript.scala b/hkmc2/js/src/main/scala/hkmc2/MLscript.scala new file mode 100644 index 0000000000..d281cfe6bb --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/MLscript.scala @@ -0,0 +1,45 @@ +package hkmc2 + +import scala.util.Try +import scala.scalajs.js.annotation.* +import org.scalajs.dom +import org.scalajs.dom.document +import mlscript.utils._ +import mlscript.utils.shorthands._ +import scala.util.matching.Regex +import scala.scalajs.js +import scala.collection.immutable + +import io.* +import scala.collection.mutable.{ArrayBuffer, Buffer} + +@JSExportTopLevel("MLscript") +class MLscript: + private val fs = InMemoryFileSystem: + Map("/Prelude.mls" -> generated.MLscript.preludeFile) + + private given Config = Config.default.copy(rewriteWhileLoops = false) + + private given FileSystem = fs + + private val compiler = MLsCompiler(Path("/Prelude.mls"), newOutputBlock) + + private val outputBlocks = Buffer.empty[Array[String]] + + private def newOutputBlock(start: (Str => Unit) => Unit): Unit = + val lines = ArrayBuffer.empty[String] + start(lines.append) + outputBlocks += lines.toArray + + @JSExport + def compile(content: Str): Unit = + val filePath = "/main.mls" + fs.addFile(filePath, content) + println(s"All files (before compilation): ${fs.allFiles.keys.mkString(", ")}") + compiler.compileModule(Path(filePath)) + println(s"All files (after compilation): ${fs.allFiles.keys.mkString(", ")}") + println(s"Output blocks:") + for lines <- outputBlocks do + println(lines.mkString("\n")) + println("Compiled JavaScript:") + println(fs.read(Path("/main.mjs"))) diff --git a/hkmc2/js/src/main/scala/hkmc2/Main.scala b/hkmc2/js/src/main/scala/hkmc2/Main.scala deleted file mode 100644 index 6f0bf29e3d..0000000000 --- a/hkmc2/js/src/main/scala/hkmc2/Main.scala +++ /dev/null @@ -1,16 +0,0 @@ -package hkmc2 - -import scala.util.Try -import scala.scalajs.js.annotation.* -import org.scalajs.dom -import org.scalajs.dom.document -import mlscript.utils._ -import mlscript.utils.shorthands._ -import scala.util.matching.Regex -import scala.scalajs.js -import scala.collection.immutable - -@JSExportTopLevel("MLscript") -object MLscript: - @JSExport - def compile(): String = "Hello, world!" diff --git a/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala index dc07e93db5..cab9de8bec 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala @@ -17,9 +17,9 @@ private object NodeFs extends js.Object: def existsSync(path: String): Boolean = js.native /** - * JavaScript implementation of FileSystem using Node.js fs module + * JavaScript implementation of [[FileSystem]] using Node.js fs module. */ -private class JsFileSystem extends FileSystem: +private class NodeFileSystem extends FileSystem: def read(path: Path): String = NodeFs.readFileSync(path.toString, "utf8") @@ -29,6 +29,5 @@ private class JsFileSystem extends FileSystem: def exists(path: Path): Bool = NodeFs.existsSync(path.toString) -// Platform-specific factory for FileSystem private[io] object PlatformFileSystem: - def default: FileSystem = new JsFileSystem + def default: FileSystem = new NodeFileSystem diff --git a/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala index f982005bbe..63817c1661 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala @@ -28,7 +28,7 @@ object NodePath extends js.Object: /** * JavaScript implementation of Path using Node.js path module */ -private[io] class JsPath(val pathString: String) extends Path: +private[io] class NodePath(val pathString: String) extends Path: private lazy val parsed = NodePath.parse(pathString) override def toString: String = pathString @@ -41,16 +41,16 @@ private[io] class JsPath(val pathString: String) extends Path: if parsed.ext.startsWith(".") then parsed.ext.substring(1) else parsed.ext - def up: Path = new JsPath(NodePath.dirname(pathString)) + def up: Path = new NodePath(NodePath.dirname(pathString)) def /(relPath: RelPath): Path = - new JsPath(NodePath.join(pathString, relPath.toString)) + new NodePath(NodePath.join(pathString, relPath.toString)) def /(fragment: String): Path = - new JsPath(pathString + NodePath.sep + fragment) + new NodePath(pathString + NodePath.sep + fragment) def relativeTo(base: Path): Opt[RelPath] = - try S(new JsRelPath(NodePath.relative(base.toString, pathString))) + try S(new NodeRelPath(NodePath.relative(base.toString, pathString))) catch case _: Exception => N def segments: Ls[String] = @@ -61,20 +61,20 @@ private[io] class JsPath(val pathString: String) extends Path: /** * JavaScript implementation of RelPath using Node.js path module */ -private[io] class JsRelPath(val pathString: String) extends RelPath: +private[io] class NodeRelPath(val pathString: String) extends RelPath: override def toString: String = pathString def segments: Ls[String] = pathString.split(NodePath.sep).toList.filter(_.nonEmpty) def /(other: RelPath): RelPath = - new JsRelPath(NodePath.join(pathString, other.toString)) + new NodeRelPath(NodePath.join(pathString, other.toString)) /** * Platform-specific factory for creating Path instances */ private[io] object PathFactory: - def fromString(str: String) = new JsPath(str) + def fromString(str: String) = new NodePath(str) def separator: String = NodePath.sep - def relPathFromString(str: String) = new JsRelPath(str) - def relPathUp = new JsRelPath("..") + def relPathFromString(str: String) = new NodeRelPath(str) + def relPathUp = new NodeRelPath("..") diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala b/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala index 0c9ae990bb..2a6eaba084 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala @@ -24,3 +24,6 @@ trait FileSystem: object FileSystem: /** Get the platform default file system by delegating to the platform. */ def default: FileSystem = PlatformFileSystem.default + + class FileNotFoundException(path: Path) extends Exception: + override def getMessage(): String = s"File not found: ${path.toString}" diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala b/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala index fd9e316316..671efaa85d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala @@ -11,7 +11,7 @@ class InMemoryFileSystem(initialFiles: Map[String, String]) extends FileSystem: private val files: MutMap[String, String] = MutMap.from(initialFiles) def read(path: Path): String = - files.getOrElse(path.toString, throw new java.io.FileNotFoundException(path.toString)) + files.getOrElse(path.toString, throw new FileSystem.FileNotFoundException(path)) def write(path: Path, content: String): Unit = files(path.toString) = content diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000000..b739b76ad8 --- /dev/null +++ b/test.mjs @@ -0,0 +1,13 @@ +// Note: This file will be removed before the PR is merged. + +import { MLscript } from "./hkmc2/js/target/scala-3.7.3/hkmc2-fastopt/MLscript.mjs"; + +const compiler = new MLscript(); + +const program = ` +class Some[A](x: A) +object None +type Option[A] = Some[A] | None +`; + +console.log(compiler.compile(program)); \ No newline at end of file From 6ad6385419321b03dc9beb638d712987073685f3 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:53:01 +0800 Subject: [PATCH 03/22] Refactor compiler interface according to actual needs --- build.sbt | 30 +++- hkmc2/js/src/main/scala/hkmc2/Compiler.scala | 72 ++++++++++ .../main/scala/hkmc2/DummyFileSystem.scala | 25 ++++ hkmc2/js/src/main/scala/hkmc2/MLscript.scala | 45 ------ .../scala/hkmc2/io/InMemoryFileSystem.scala | 14 +- .../main/scala/hkmc2/io/PlatformPath.scala | 80 +---------- .../src/main/scala/hkmc2/io/VirtualPath.scala | 114 +++++++++++++++ .../io/{ => node}/PlatformFileSystem.scala | 0 .../scala/hkmc2/io/node/PlatformPath.scala | 81 +++++++++++ hkmc2/js/src/main/scala/hkmc2/std.scala | 33 +++++ .../scala/hkmc2/io/VirtualPathTests.scala | 132 ++++++++++++++++++ .../test/scala/hkmc2/CompileTestRunner.scala | 31 ++-- .../src/main/scala/hkmc2/MLsCompiler.scala | 52 ++++--- .../src/main/scala/hkmc2/semantics/Term.scala | 75 ++++++++++ .../src/test/mlscript-compile/Predef.mjs | 1 - .../src/test/mlscript-compile/Runtime.mjs | 1 - .../src/test/mlscript-compile/RuntimeJS.mjs | 2 - .../src/test/scala/hkmc2/Watcher.scala | 19 ++- test.mjs | 17 ++- 19 files changed, 656 insertions(+), 168 deletions(-) create mode 100644 hkmc2/js/src/main/scala/hkmc2/Compiler.scala create mode 100644 hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala delete mode 100644 hkmc2/js/src/main/scala/hkmc2/MLscript.scala rename hkmc2/{shared => js}/src/main/scala/hkmc2/io/InMemoryFileSystem.scala (66%) create mode 100644 hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala rename hkmc2/js/src/main/scala/hkmc2/io/{ => node}/PlatformFileSystem.scala (100%) create mode 100644 hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala create mode 100644 hkmc2/js/src/main/scala/hkmc2/std.scala create mode 100644 hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala diff --git a/build.sbt b/build.sbt index dfe7c9da02..93312ef9f9 100644 --- a/build.sbt +++ b/build.sbt @@ -59,9 +59,13 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) .jvmSettings( ) .jsSettings( + Compile / fastOptJS / artifactPath := + baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript-compile"/"apps"/"web-demo"/"build"/"MLscript.mjs", + Compile / fullOptJS / artifactPath := + baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript-compile"/"apps"/"web-demo"/"build"/"MLscript.mjs", scalaJSLinkerConfig ~= { - _.withModuleKind(ModuleKind.ESModule) - .withOutputPatterns(OutputPatterns.fromJSFile("MLscript.mjs")) + _.withModuleKind(ModuleKind.ESModule).withMinify(true) + // .withOutputPatterns(OutputPatterns.fromJSFile("MLscript.mjs")) }, libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.2.0", // We directly read the necessary MLscript files from the test folders to @@ -77,12 +81,18 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) val testFolder = baseDirectory.value / ".." / "shared" / "src" / "test" val compileFolder = testFolder / "mlscript-compile" val preludeFile = IO.read(testFolder / "mlscript" / "decls" / "Prelude.mls") - val stdFiles = List("Predef", "Runtime", "Rendering", "Stack", "Iter", "Option") + val stdFiles = List("Char", "FingerTreeList", "Iter", "LazyArray", + "LazyFingerTree", "MutMap", "ObjectBuffer", "Option", "Predef", + "Record", "Rendering", "Runtime", "RuntimeJS", "Stack", "StrOps", + "Term", "TreeTracer", "XML") .iterator .map { fileName => (compileFolder / s"$fileName.mls", compileFolder / s"$fileName.mjs") }.map { case (mlsPath, mjsPath) => - (mlsPath.getName(), IO.read(mlsPath), IO.read(mjsPath)) + // Only read when the corresponding file exists. + val mls = if (mlsPath.exists()) Some(IO.read(mlsPath)) else None + val mjs = if (mjsPath.exists()) Some(IO.read(mjsPath)) else None + (mlsPath.getName(), mls, mjs) }.toList val outFile = (Compile / sourceManaged).value / "generated" / "MLscript.scala" IO.write( @@ -91,10 +101,16 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) |import collection.mutable.Map as MutMap |object MLscript: | val preludeFile = ${escape(preludeFile)} - | val sourceFiles: MutMap[String, (String, String)] = MutMap.empty + | val sourceFiles: MutMap[String, (mls: Option[String], mjs: Option[String])] = MutMap.empty |""".stripMargin + - (stdFiles.iterator.map { case (fileName, mlsContent, mjsContent) => - s" sourceFiles += (${escape(s"/std/$fileName")} -> (${escape(mlsContent)}, ${escape(mjsContent)}))" + (stdFiles.iterator.map { case (fileName, mlsSourceOpt, mjsSourceOpt) => + val mls = mlsSourceOpt match { + case Some(source) => s"Some(${escape(source)})" + case None => "None" } + val mjs = mjsSourceOpt match { + case Some(source) => s"Some(${escape(source)})" + case None => "None" } + s" sourceFiles += (${escape(s"/std/$fileName")} -> ($mls, $mjs))" }.mkString("\n")) + "\n" ) Seq(outFile) diff --git a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala new file mode 100644 index 0000000000..374a69169c --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala @@ -0,0 +1,72 @@ +package hkmc2 + +import scala.util.Try +import scala.scalajs.js.annotation.* +import org.scalajs.dom +import org.scalajs.dom.document +import mlscript.utils._ +import mlscript.utils.shorthands._ +import scala.util.matching.Regex +import scala.scalajs.js, js.JSConverters.* +import scala.collection.immutable +import scala.collection.mutable.Map as MutMap + +import io.* +import scala.collection.mutable.{ArrayBuffer, Buffer} + +@JSExportTopLevel("Compiler") +class Compiler(fs: FileSystem, paths: MLsCompiler.Paths): + private given Config = Config.default.copy(rewriteWhileLoops = false) + + private given FileSystem = fs + + private var pathDiagnosticsMap = MutMap.empty[Str, (Int, Buffer[Diagnostic])] + + private def mkRaise(path: io.Path): Raise = d => + pathDiagnosticsMap.getOrElseUpdate(path.toString, (pathDiagnosticsMap.size, Buffer.empty))._2 += d + + private val compiler = MLsCompiler(paths, mkRaise) + + private def collectDiagnostics(): js.Array[js.Dynamic] = + pathDiagnosticsMap.iterator.toArray.sortBy(_._2._1).map: + case (path, (_, diagnostics)) => js.Dynamic.literal( + path = path, + diagnostics = diagnostics.map: d => + js.Dynamic.literal( + kind = d.kind.toString().toLowerCase(), + source = d.source.toString().toLowerCase(), + mainMessage = d.theMsg, + allMessages = d.allMsgs.map: + case (message, loc) => + lazy val ctx = ShowCtx.mk: + message.bits.collect: + case Message.Code(t) => t + js.Dynamic.literal( + messageBits = message.bits.map: + case Message.Text(text) => js.Dynamic.literal(text = text) + case Message.Code(ty) => ty.showIn(0)(using ctx) + .toJSArray, + location = loc match + case S(loc) => js.Dynamic.literal( + start = loc.spanStart, + end = loc.spanEnd + ) + case N => null + ) + .toJSArray + ) + .toJSArray) + .toJSArray + + @JSExport + def compile(filePath: Str): js.Array[js.Dynamic] = + compiler.compileModule(Path(filePath)) + val perFileDiagnostics = collectDiagnostics() + pathDiagnosticsMap = MutMap.empty + perFileDiagnostics + +@JSExportTopLevel("Paths") +final class Paths(prelude: Str, runtime: Str, term: Str) extends MLsCompiler.Paths: + val preludeFile = Path(prelude) + val runtimeFile = Path(runtime) + val termFile = Path(term) diff --git a/hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala new file mode 100644 index 0000000000..c46d7f5abf --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala @@ -0,0 +1,25 @@ +package hkmc2 + +import scala.scalajs.js.annotation.* +import scala.scalajs.js, js.JSConverters.* +import mlscript.utils.*, shorthands.* +import io.* + +/** + * Provide a wrapper for virtual file system implemented in JavaScript. + * + * @param module the JavaScript objct representing the file system + */ +@JSExportTopLevel("DummyFileSystem") +class DummyFileSystem(module: js.Dynamic) extends io.FileSystem: + /** Read entire file as string. */ + def read(path: Path): String = + module.read(path.toString).asInstanceOf[String] + + /** Write string to file, overwriting if exists. */ + def write(path: Path, content: String): Unit = + module.write(path.toString, content) + + /** Check if a file exists at the given path. */ + def exists(path: Path): Bool = + module.exists(path.toString).asInstanceOf[Bool] diff --git a/hkmc2/js/src/main/scala/hkmc2/MLscript.scala b/hkmc2/js/src/main/scala/hkmc2/MLscript.scala deleted file mode 100644 index d281cfe6bb..0000000000 --- a/hkmc2/js/src/main/scala/hkmc2/MLscript.scala +++ /dev/null @@ -1,45 +0,0 @@ -package hkmc2 - -import scala.util.Try -import scala.scalajs.js.annotation.* -import org.scalajs.dom -import org.scalajs.dom.document -import mlscript.utils._ -import mlscript.utils.shorthands._ -import scala.util.matching.Regex -import scala.scalajs.js -import scala.collection.immutable - -import io.* -import scala.collection.mutable.{ArrayBuffer, Buffer} - -@JSExportTopLevel("MLscript") -class MLscript: - private val fs = InMemoryFileSystem: - Map("/Prelude.mls" -> generated.MLscript.preludeFile) - - private given Config = Config.default.copy(rewriteWhileLoops = false) - - private given FileSystem = fs - - private val compiler = MLsCompiler(Path("/Prelude.mls"), newOutputBlock) - - private val outputBlocks = Buffer.empty[Array[String]] - - private def newOutputBlock(start: (Str => Unit) => Unit): Unit = - val lines = ArrayBuffer.empty[String] - start(lines.append) - outputBlocks += lines.toArray - - @JSExport - def compile(content: Str): Unit = - val filePath = "/main.mls" - fs.addFile(filePath, content) - println(s"All files (before compilation): ${fs.allFiles.keys.mkString(", ")}") - compiler.compileModule(Path(filePath)) - println(s"All files (after compilation): ${fs.allFiles.keys.mkString(", ")}") - println(s"Output blocks:") - for lines <- outputBlocks do - println(lines.mkString("\n")) - println("Compiled JavaScript:") - println(fs.read(Path("/main.mjs"))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala similarity index 66% rename from hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala rename to hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala index 671efaa85d..83878d1ace 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/io/InMemoryFileSystem.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala @@ -2,6 +2,7 @@ package hkmc2.io import mlscript.utils._, shorthands._ import collection.mutable.Map as MutMap +import scala.scalajs.js, js.annotation.JSExport, js.JSConverters.* /** * In-memory file system for testing and web compiler. Stores files as a map @@ -14,13 +15,22 @@ class InMemoryFileSystem(initialFiles: Map[String, String]) extends FileSystem: files.getOrElse(path.toString, throw new FileSystem.FileNotFoundException(path)) def write(path: Path, content: String): Unit = + print(s"Writing to $path") files(path.toString) = content def exists(path: Path): Bool = files.contains(path.toString) - /** Add a file to the virtual file system (for testing) */ - def addFile(path: String, content: String): Unit = + @JSExport("write") + def write(path: Str, content: Str): Unit = + print(s"I'm writing to $path") files(path) = content + @JSExport("read") + def read(path: Str): Str = + files.getOrElse(path, throw new FileSystem.FileNotFoundException(Path(path))) + + @JSExport("list") + def list: js.Array[Str] = allFiles.keys.toJSArray + /** Get all files (for debugging) */ def allFiles: Map[String, String] = files.toMap diff --git a/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala index 63817c1661..1d1d732392 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala @@ -1,80 +1,10 @@ -package hkmc2 -package io - -import scala.scalajs.js -import scala.scalajs.js.annotation._ - -import mlscript.utils._, shorthands._ - -@js.native -trait ParsedPath extends js.Object: - val base: String = js.native - val name: String = js.native - val ext: String = js.native - -/** - * Node.js path module facade - */ -@js.native -@JSImport("path", JSImport.Namespace) -object NodePath extends js.Object: - def sep: String = js.native - def parse(path: String): ParsedPath = js.native - def relative(from: String, to: String): String = js.native - def join(paths: String*): String = js.native - def isAbsolute(path: String): Boolean = js.native - def dirname(path: String): String = js.native - -/** - * JavaScript implementation of Path using Node.js path module - */ -private[io] class NodePath(val pathString: String) extends Path: - private lazy val parsed = NodePath.parse(pathString) - - override def toString: String = pathString - - def last: String = parsed.base - - def baseName: String = parsed.name - - def ext: String = - if parsed.ext.startsWith(".") then parsed.ext.substring(1) - else parsed.ext - - def up: Path = new NodePath(NodePath.dirname(pathString)) - - def /(relPath: RelPath): Path = - new NodePath(NodePath.join(pathString, relPath.toString)) - - def /(fragment: String): Path = - new NodePath(pathString + NodePath.sep + fragment) - - def relativeTo(base: Path): Opt[RelPath] = - try S(new NodeRelPath(NodePath.relative(base.toString, pathString))) - catch case _: Exception => N - - def segments: Ls[String] = - pathString.split(NodePath.sep).toList.filter(_.nonEmpty) - - def isAbsolute: Bool = NodePath.isAbsolute(pathString) - -/** - * JavaScript implementation of RelPath using Node.js path module - */ -private[io] class NodeRelPath(val pathString: String) extends RelPath: - override def toString: String = pathString - - def segments: Ls[String] = - pathString.split(NodePath.sep).toList.filter(_.nonEmpty) - - def /(other: RelPath): RelPath = - new NodeRelPath(NodePath.join(pathString, other.toString)) +package hkmc2.io /** * Platform-specific factory for creating Path instances */ private[io] object PathFactory: - def fromString(str: String) = new NodePath(str) - def separator: String = NodePath.sep - def relPathFromString(str: String) = new NodeRelPath(str) - def relPathUp = new NodeRelPath("..") + def fromString(str: String) = new VirtualPath(str) + def separator: String = VirtualPath.sep + def relPathFromString(str: String) = new VirtualRelPath(str) + def relPathUp = new VirtualRelPath("..") diff --git a/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala new file mode 100644 index 0000000000..0cb0709601 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala @@ -0,0 +1,114 @@ +package hkmc2.io + +import scala.scalajs.js +import mlscript.utils._, shorthands._ +import VirtualPath.sep + +/** + * Pure JavaScript implementation of Path without using Node.js path module + */ +private[io] class VirtualPath(val pathString: String) extends Path: + private def normalizePath(path: String): String = + if path.isEmpty then path + else + // Split by separator and filter out empty segments + val segments = path.split(sep).filter(_.nonEmpty) + val isAbs = path.startsWith(sep) + + // Resolve . and .. segments + val normalized = segments.foldLeft(List.empty[String]) { (acc, seg) => + seg match + case "." => acc // Current directory, skip it + case ".." => + // Parent directory, pop the last segment if possible + if acc.isEmpty || acc.last == ".." then acc :+ seg + else acc.dropRight(1) + case _ => acc :+ seg + } + + if isAbs then sep + normalized.mkString(sep) + else if normalized.isEmpty then "." + else normalized.mkString(sep) + + override def toString: String = pathString + + def last: String = + val idx = pathString.lastIndexOf(sep) + if idx < 0 then pathString + else pathString.substring(idx + 1) + + def baseName: String = + val filename = last + val dotIdx = filename.lastIndexOf('.') + if dotIdx <= 0 then filename // .hidden files or no extension + else filename.substring(0, dotIdx) + + def ext: String = + val filename = last + val dotIdx = filename.lastIndexOf('.') + if dotIdx <= 0 then "" // .hidden files or no extension + else filename.substring(dotIdx + 1) + + def up: Path = + val idx = pathString.lastIndexOf(sep) + if idx < 0 then new VirtualPath(".") + else if idx == 0 then new VirtualPath(sep) // root case + else new VirtualPath(pathString.substring(0, idx)) + + def /(relPath: RelPath): Path = + val combined = if pathString.endsWith(sep) then + pathString + relPath.toString + else + pathString + sep + relPath.toString + new VirtualPath(normalizePath(combined)) + + def /(fragment: String): Path = + val combined = if pathString.endsWith(sep) then + pathString + fragment + else + pathString + sep + fragment + new VirtualPath(normalizePath(combined)) + + def relativeTo(base: Path): Opt[RelPath] = + try + val baseSegs = base.segments + val targetSegs = segments + + // Find common prefix + var i = 0 + while i < baseSegs.length && i < targetSegs.length && baseSegs(i) == targetSegs(i) do + i += 1 + + // Build relative path + val upCount = baseSegs.length - i + val ups = List.fill(upCount)("..") + val downs = targetSegs.drop(i) + + val relSegs = ups ++ downs + if relSegs.isEmpty then S(new VirtualRelPath(".")) + else S(new VirtualRelPath(relSegs.mkString(sep))) + catch case _: Exception => N + + def segments: Ls[String] = + pathString.split(sep).toList.filter(_.nonEmpty) + + def isAbsolute: Bool = pathString.startsWith(sep) + +private[io] object VirtualPath: + val sep = "/" + +/** + * Pure JavaScript implementation of RelPath without using Node.js path module + */ +private[io] class VirtualRelPath(val pathString: String) extends RelPath: + override def toString: String = pathString + + def segments: Ls[String] = + pathString.split(sep).toList.filter(_.nonEmpty) + + def /(other: RelPath): RelPath = + val combined = if pathString.endsWith(sep) then + pathString + other.toString + else + pathString + sep + other.toString + new VirtualRelPath(combined) diff --git a/hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala similarity index 100% rename from hkmc2/js/src/main/scala/hkmc2/io/PlatformFileSystem.scala rename to hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala diff --git a/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala new file mode 100644 index 0000000000..1b0a414224 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala @@ -0,0 +1,81 @@ +package hkmc2 +package io +package node + +import scala.scalajs.js +import scala.scalajs.js.annotation._ + +import mlscript.utils._, shorthands._ + +@js.native +trait ParsedPath extends js.Object: + val base: String = js.native + val name: String = js.native + val ext: String = js.native + +/** + * Node.js path module facade + */ +@js.native +@JSImport("path", JSImport.Namespace) +object NodePath extends js.Object: + def sep: String = js.native + def parse(path: String): ParsedPath = js.native + def relative(from: String, to: String): String = js.native + def join(paths: String*): String = js.native + def isAbsolute(path: String): Boolean = js.native + def dirname(path: String): String = js.native + +/** + * JavaScript implementation of Path using Node.js path module + */ +private[io] class NodePath(val pathString: String) extends Path: + private lazy val parsed = NodePath.parse(pathString) + + override def toString: String = pathString + + def last: String = parsed.base + + def baseName: String = parsed.name + + def ext: String = + if parsed.ext.startsWith(".") then parsed.ext.substring(1) + else parsed.ext + + def up: Path = new NodePath(NodePath.dirname(pathString)) + + def /(relPath: RelPath): Path = + new NodePath(NodePath.join(pathString, relPath.toString)) + + def /(fragment: String): Path = + new NodePath(pathString + NodePath.sep + fragment) + + def relativeTo(base: Path): Opt[RelPath] = + try S(new NodeRelPath(NodePath.relative(base.toString, pathString))) + catch case _: Exception => N + + def segments: Ls[String] = + pathString.split(NodePath.sep).toList.filter(_.nonEmpty) + + def isAbsolute: Bool = NodePath.isAbsolute(pathString) + +/** + * JavaScript implementation of RelPath using Node.js path module + */ +private[io] class NodeRelPath(val pathString: String) extends RelPath: + override def toString: String = pathString + + def segments: Ls[String] = + pathString.split(NodePath.sep).toList.filter(_.nonEmpty) + + def /(other: RelPath): RelPath = + new NodeRelPath(NodePath.join(pathString, other.toString)) + +/** + * Platform-specific factory for creating Path instances + */ +private[io] object PathFactory: + def fromString(str: String) = new NodePath(str) + def separator: String = NodePath.sep + def relPathFromString(str: String) = new NodeRelPath(str) + def relPathUp = new NodeRelPath("..") diff --git a/hkmc2/js/src/main/scala/hkmc2/std.scala b/hkmc2/js/src/main/scala/hkmc2/std.scala new file mode 100644 index 0000000000..54bfd8d6fe --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/std.scala @@ -0,0 +1,33 @@ +package hkmc2 + +import io.*, mlscript.utils.*, shorthands.* +import scala.scalajs.js, js.annotation.*, js.JSConverters.* +import scala.collection.mutable.Buffer + +@JSExportTopLevel("std") +object std: + @JSExport + val prelude = generated.MLscript.preludeFile + + @JSExport + val files = + val buffer = Buffer.empty[js.Tuple2[String, String]] + generated.MLscript.sourceFiles.foreach: + case (fileName, (mlsSourceOpt, mjsSourceOpt)) => + mlsSourceOpt match + case Some(mlsSource) => + buffer += js.Tuple2(fileName, mlsSource) + case None => () + mjsSourceOpt match + case Some(mjsSource) => + buffer += js.Tuple2(fileName.dropRight(3) + "mjs", mjsSource) + case None => () + buffer.toJSArray + + @JSExport + def defaultFileSystem: InMemoryFileSystem = + new InMemoryFileSystem(files.map(t => (t._1, t._2)).toMap + ("/Prelude.mls" -> prelude)) + + @JSExport + def defaultPaths: Paths = + new Paths("/Prelude.mls", "/Runtime.mjs", "/Term.mjs") \ No newline at end of file diff --git a/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala b/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala new file mode 100644 index 0000000000..a9a3d902b8 --- /dev/null +++ b/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala @@ -0,0 +1,132 @@ +package hkmc2.io + +import org.scalatest.funsuite.AnyFunSuite +import mlscript.utils._, shorthands._ + +class VirtualPathTests extends AnyFunSuite: + + test("basic path creation and toString"): + val path = VirtualPath("foo/bar") + assert(path.toString == "foo/bar") + + test("/ operator with simple fragment"): + val path = VirtualPath("foo") + val result = path / "bar" + assert(result.toString == "foo/bar") + + test("/ operator with fragment starting with ."): + val path = VirtualPath("foo") + val result = path / "./bar" + assert(result.toString == "foo/bar", "Current directory '.' should be removed") + + test("/ operator with fragment starting with ./"): + val path = VirtualPath("foo/baz") + val result = path / "./bar" + assert(result.toString == "foo/baz/bar", "Current directory '.' should be removed") + + test("/ operator with fragment containing .."): + val path = VirtualPath("foo/baz") + val result = path / "../bar" + assert(result.toString == "foo/bar", "Parent directory '..' should navigate up one level") + + test("/ operator with multiple .. segments"): + val path = VirtualPath("a/b/c") + val result = path / "../../d" + assert(result.toString == "a/d", "Multiple '..' should navigate up multiple levels") + + test("/ operator with . in the middle of path"): + val path = VirtualPath("foo") + val result = path / "bar/./baz" + assert(result.toString == "foo/bar/baz", "Current directory '.' in middle should be removed") + + test("/ operator with .. in the middle of path"): + val path = VirtualPath("foo") + val result = path / "bar/../baz" + assert(result.toString == "foo/baz", "Parent directory '..' in middle should collapse segments") + + test("/ operator with absolute path"): + val path = VirtualPath("/abs/path") + val result = path / "./file.txt" + assert(result.toString == "/abs/path/file.txt", "Should work with absolute paths") + + test("/ operator with RelPath containing ."): + val path = VirtualPath("foo") + val relPath = VirtualRelPath("./bar") + val result = path / relPath + assert(result.toString == "foo/bar", "RelPath with '.' should be normalized") + + test("/ operator with RelPath containing .."): + val path = VirtualPath("foo/baz") + val relPath = VirtualRelPath("../bar") + val result = path / relPath + assert(result.toString == "foo/bar", "RelPath with '..' should be normalized") + + test("/ operator with RelPath containing multiple . and .."): + val path = VirtualPath("a/b") + val relPath = VirtualRelPath("./c/../d") + val result = path / relPath + assert(result.toString == "a/b/d", "Complex RelPath should be normalized") + + test("normalization with too many .. segments"): + val path = VirtualPath("foo") + val result = path / "../../bar" + assert(result.toString == "../bar", "Extra '..' should be preserved for relative paths") + + test("normalization resulting in just ."): + val path = VirtualPath("foo") + val result = path / ".." + assert(result.toString == ".", "Navigating up from single segment should result in '.'") + + test("/ operator with trailing slash"): + val path = VirtualPath("foo/") + val result = path / "bar" + assert(result.toString == "foo/bar", "Should handle trailing slash correctly") + + test("path segments"): + val path = VirtualPath("foo/bar/baz") + assert(path.segments == List("foo", "bar", "baz")) + + test("last segment"): + val path = VirtualPath("foo/bar/baz.txt") + assert(path.last == "baz.txt") + + test("baseName"): + val path = VirtualPath("foo/bar/baz.txt") + assert(path.baseName == "baz") + + test("ext"): + val path = VirtualPath("foo/bar/baz.txt") + assert(path.ext == "txt") + + test("up"): + val path = VirtualPath("foo/bar/baz") + assert(path.up.toString == "foo/bar") + + test("isAbsolute - relative path"): + val path = VirtualPath("foo/bar") + assert(!path.isAbsolute) + + test("isAbsolute - absolute path"): + val path = VirtualPath("/foo/bar") + assert(path.isAbsolute) + + test("complex normalization case"): + val path = VirtualPath("a/b/c") + val result = path / "./d/../e/./f" + assert(result.toString == "a/b/c/e/f", "Complex path with mixed . and .. should normalize correctly") + + test("normalization with only ."): + val path = VirtualPath("foo") + val result = path / "." + assert(result.toString == "foo", "Single '.' should result in same path") + + test("normalization preserves absolute paths"): + val path = VirtualPath("/a/b") + val result = path / "../c" + assert(result.toString == "/a/c", "Absolute paths should remain absolute after normalization") + + test("RelPath / operator"): + val rel1 = VirtualRelPath("foo/bar") + val rel2 = VirtualRelPath("baz") + val result = rel1 / rel2 + assert(result.toString == "foo/bar/baz") diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala index a457d89f76..01511a19b1 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala @@ -47,25 +47,38 @@ class CompileTestRunner println(s"Compiling: $relativeName") - val preludePath = mainTestDir/"mlscript"/"decls"/"Prelude.mls" - // Stack safety relies on the fact that runtime uses while loops for resumption // and does not create extra stack depth. Hence we disable while loop rewriting here. given Config = Config.default.copy(rewriteWhileLoops = false) given io.FileSystem = io.FileSystem.default + // * The weird type of `mkOutput` is to allow wrapping the reporting of + // * diagnostics in synchronized blocks. + // TODO: Fix the weird type, which should be unnecessary in `Watcher`. + val mkOutput = (outputConsumer: (Str => Unit) => Unit) => + // * Synchronize diagnostic output to avoid interleaving since the compiler tests run in parallel + CompileTestRunner.synchronized: + outputConsumer(System.out.println) + val report = ReportFormatter(mkOutput) + def mkRaise(file: io.Path): Raise = + val wd = file.up + d => mkOutput: + val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) + _(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) + report(0, d :: Nil, showRelativeLineNums = false) + val compiler = MLsCompiler( - preludePath, - mkOutput => - // * Synchronize diagnostic output to avoid interleaving since the compiler tests run in parallel - CompileTestRunner.synchronized: - mkOutput(System.out.println) + new MLsCompiler.Paths: + val preludeFile = mainTestDir / "mlscript" / "decls" / "Prelude.mls" + val runtimeFile = mainTestDir / "mlscript-compile" / "Runtime.mjs" + val termFile = mainTestDir / "mlscript-compile" / "Term.mjs", + mkRaise ) compiler.compileModule(file) - if compiler.report.badLines.nonEmpty then + if report.badLines.nonEmpty then fail(s"Unexpected diagnostic at: " + - compiler.report.badLines.distinct.sorted + report.badLines.distinct.sorted .map("\n\t"+relativeName+"."+file.ext+":"+_).mkString(", ")) } diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 311307f09b..f98d6e10c3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -9,6 +9,7 @@ import utils.* import hkmc2.semantics.MemberSymbol import hkmc2.semantics.Elaborator import hkmc2.semantics.Resolver +import hkmc2.semantics.{Import, Term} import hkmc2.syntax.Keyword.`override` import semantics.Elaborator.{Ctx, State} @@ -34,21 +35,26 @@ class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State val result = parser.parseAll(parser.block(allowNewlines = true)) val resultBlk = new syntax.Tree.Block(result) - - -// * The weird type of `mkOutput` is to allow wrapping the reporting of diagnostics in synchronized blocks -class MLsCompiler(preludeFile: io.Path, mkOutput: ((Str => Unit) => Unit) => Unit)(using cfg: Config, fs: io.FileSystem): +object MLsCompiler: + /** The class contains the necessary paths to files for the MLscript compiler. */ + trait Paths: + def preludeFile: io.Path + def runtimeFile: io.Path + def termFile: io.Path - val runtimeFile: io.Path = preludeFile.up.up.up / io.RelPath("mlscript-compile/Runtime.mjs") - val termFile: io.Path = preludeFile.up.up.up / io.RelPath("mlscript-compile/Term.mjs") +/** + * The compiler that compiles MLscript code into JavaScript modules. + * + * @param paths required paths needed by the compiler + * @param mkRaise generates a separate `Raise` function for each file. + * @param config the compiler's configuration object + * @param fs the file system interface + */ +class MLsCompiler(paths: MLsCompiler.Paths, mkRaise: io.Path => Raise)(using config: Config, fs: io.FileSystem): + import paths.* - val report = ReportFormatter: outputConsumer => - mkOutput: output => - outputConsumer: str => - output(fansi.Color.Red(str).toString) - // TODO adapt logic val etl = new TraceLogger{override def doTrace: Bool = false} @@ -60,14 +66,10 @@ class MLsCompiler(preludeFile: io.Path, mkOutput: ((Str => Unit) => Unit) => Uni def compileModule(file: io.Path): Unit = - + val wd = file.up - - given raise: Raise = d => - mkOutput: - val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) - _(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) - report(0, d :: Nil, showRelativeLineNums = false) + + given Raise = mkRaise(file) given Elaborator.State = new Elaborator.State @@ -86,10 +88,16 @@ class MLsCompiler(preludeFile: io.Path, mkOutput: ((Str => Unit) => Unit) => Uni val (blk0, _) = elab.importFrom(parsed) val resolver = Resolver(rtl) resolver.traverseBlock(blk0)(using Resolver.ICtx.empty) - val blk = new semantics.Term.Blk( - semantics.Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) - :: semantics.Import(State.termSymbol, termFile.toString, termFile) - :: blk0.stats, + val hasQuote = blk0.exists: + case Term.Quoted(_) | Term.Unquoted(_) => true + case Term.Ref(sym) => sym === State.termSymbol + val blk = new Term.Blk( + Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: + // Only import `Term.mls` when necessary. + (if hasQuote then + Import(State.termSymbol, termFile.toString, termFile) :: blk0.stats + else + blk0.stats), blk0.res ) val low = ltl.givenIn: diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index bffc1091af..072158100f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -511,6 +511,81 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: case Neg(e) => e :: Nil case Annotated(ann, target) => ann.subTerms ::: target :: Nil + /** Check if the term satisfies the predicate. The reason I did not use + * `subTerms` and `subStatements` for traversal is that they consume a + * considerable amount of time and memory, and they fail to terminate on + * `Rule.mls`. */ + def exists(p: PartialFunction[Statement, Bool]): Bool = + def go(stmt: Statement): Bool = + stmt match + case _ if p.isDefinedAt(stmt) => p(stmt) + case Error | _: Lit | _: UnitVal | Missing | _: Ref => false + case App(lhs, rhs) => go(lhs) || go(rhs) + case RcdField(lhs, rhs) => go(lhs) || go(rhs) + case RcdSpread(bod) => go(bod) + case FunTy(lhs, rhs, eff) => go(lhs) || go(rhs) || eff.exists(go) + case TyApp(pre, tarsg) => go(pre) || tarsg.exists(go) + case Sel(pre, _) => go(pre) + case SynthSel(pre, _) => go(pre) + case DynSel(o, f, _) => go(o) || go(f) + case Tup(fields) => fields.exists(_.subTerms.exists(go)) + case IfLike(_, body) => body.subTerms.exists(go) + case Lam(params, body) => params.params.exists(_.subTerms.exists(go)) || go(body) + case Blk(stats, res) => stats.exists(go) || go(res) + case Rcd(mut, stats) => stats.exists(go) + case Quoted(term) => go(term) + case Unquoted(term) => go(term) + case New(cls, argss, rft) => + go(cls) || argss.exists(_.exists(go(_))) || + rft.exists(_._2.blk.subTerms.exists(go)) + case SelProj(pre, cls, _) => go(pre) || go(cls) + case Asc(term, ty) => go(term) || go(ty) + case Ret(res) => go(res) + case Throw(res) => go(res) + case Forall(_, _, body) => go(body) + case WildcardTy(in, out) => in.exists(go) || out.exists(go) + case CompType(lhs, rhs, _) => go(lhs) || go(rhs) + case LetDecl(sym, annotations) => annotations.flatMap(_.subTerms).exists(go) + case DefineVar(sym, rhs) => go(rhs) + case Region(_, body) => go(body) + case RegRef(reg, value) => go(reg) || go(value) + case Assgn(lhs, rhs) => go(lhs) || go(rhs) + case SetRef(lhs, rhs) => go(lhs) || go(rhs) + case Deref(term) => go(term) + case TermDefinition(_, _, _, pss, tps, sign, body, _, _, _, annotations, _) => + pss.toList.flatMap(_.subTerms).exists(go) || tps.getOrElse(Nil).exists: + case Param(_, _, sign, _) => sign.exists(go) + || sign.toList.exists(go) || body.exists(go) || annotations.exists: + case Annot.Trm(term) => go(term) + case Annot.Untyped | Annot.Modifier(_) => false + case cls: ClassDef => + cls.paramsOpt.toList.flatMap(_.subTerms).exists(go) || + go(cls.body.blk) || + cls.annotations.flatMap(_.subTerms).exists(go) + case mod: ModuleOrObjectDef => + mod.paramsOpt.toList.flatMap(_.subTerms).exists(go) || + go(mod.body.blk) || + mod.annotations.flatMap(_.subTerms).exists(go) + case td: TypeDef => + td.rhs.toList.exists(go) || td.annotations.flatMap(_.subTerms).exists(go) + case pat: PatternDef => + pat.paramsOpt.toList.flatMap(_.subTerms).exists(go) || + go(pat.body.blk) || + pat.annotations.flatMap(_.subTerms).exists(go) + case Import(sym, str, file) => false + case Try(body, finallyDo) => go(body) && go(finallyDo) + case Handle(lhs, rhs, args, derivedClsSym, defs, bod) => + go(rhs) || args.exists(go) || defs.flatMap(_.td.subTerms).exists(go) || go(bod) + case Neg(e) => go(e) + case Annotated(ann, target) => ann.subTerms.exists(go) || go(target) + case Mut(underlying) => go(underlying) + case DynNew(cls, args) => go(cls) && args.exists(go) + case Resolved(t, sym) => go(t) + case CtxTup(fields) => fields.exists(_.subTerms.exists(go)) + case SynthIf(split) => split.subTerms.exists(go) + case Drop(trm) => go(trm) + go(this) + // private def treeOrSubterms(t: Tree, t: Term): Ls[Located] = t match private def treeOrSubterms(t: Tree): Ls[Located] = t match case Tree.DummyApp | Tree.DummyTup => subTerms diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs index a4ce900a78..a5ea7bd598 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs @@ -1,7 +1,6 @@ const definitionMetadata = globalThis.Symbol.for("mlscript.definitionMetadata"); const prettyPrint = globalThis.Symbol.for("mlscript.prettyPrint"); import runtime from "./Runtime.mjs"; -import Term from "./Term.mjs"; import RuntimeJS from "./RuntimeJS.mjs"; import Runtime from "./Runtime.mjs"; import Rendering from "./Rendering.mjs"; diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index d5cc8a4258..35d524df37 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -1,7 +1,6 @@ const definitionMetadata = globalThis.Symbol.for("mlscript.definitionMetadata"); const prettyPrint = globalThis.Symbol.for("mlscript.prettyPrint"); import runtime from "./Runtime.mjs"; -import Term from "./Term.mjs"; import RuntimeJS from "./RuntimeJS.mjs"; import Rendering from "./Rendering.mjs"; import LazyArray from "./LazyArray.mjs"; diff --git a/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs b/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs index 66e0f06ac4..ce12c69ce6 100644 --- a/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs @@ -1,5 +1,3 @@ -import Predef from "./Predef.mjs"; - const RuntimeJS = { bitand(lhs, rhs) { return lhs & rhs; diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index a01348951a..715d303b27 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -99,7 +99,24 @@ class Watcher(dirs: Ls[File]): then given Config = Config.default given FileSystem = FileSystem.default - MLsCompiler(preludePath, outputConsumer => outputConsumer(System.out.println)).compileModule(path) + // * The weird type of `mkOutput` is to allow wrapping the reporting of + // * diagnostics in synchronized blocks. + // TODO: Fix the weird type, which should be unnecessary in `Watcher`. + val mkOutput = (outputConsumer: (Str => Unit) => Unit) => + outputConsumer(System.out.println) + val report = ReportFormatter(mkOutput) + def mkRaise(file: io.Path): Raise = + val wd = file.up + d => mkOutput: + val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) + _(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) + report(0, d :: Nil, showRelativeLineNums = false) + // Necessary paths used by the compiler. + val paths = new MLsCompiler.Paths: + val preludeFile = preludePath + val runtimeFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Runtime.mjs" + val termFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Term.mjs" + MLsCompiler(paths, mkRaise).compileModule(path) else val dm = new MainDiffMaker(rootPath.toString, path, preludePath, predefPath, relativeName): override def fs = FileSystem.default diff --git a/test.mjs b/test.mjs index b739b76ad8..1f23751acf 100644 --- a/test.mjs +++ b/test.mjs @@ -1,8 +1,12 @@ // Note: This file will be removed before the PR is merged. -import { MLscript } from "./hkmc2/js/target/scala-3.7.3/hkmc2-fastopt/MLscript.mjs"; +import * as mlscript from "./hkmc2/shared/src/test/mlscript-compile/apps/web-demo/build/MLscript.mjs"; -const compiler = new MLscript(); +const fs = mlscript.std.defaultFileSystem +const paths = mlscript.std.defaultPaths +const compiler = new mlscript.Compiler(fs, paths); + +console.log(Object.getPrototypeOf(fs)); const program = ` class Some[A](x: A) @@ -10,4 +14,11 @@ object None type Option[A] = Some[A] | None `; -console.log(compiler.compile(program)); \ No newline at end of file +fs.write("/test.mls", program); + +console.log(compiler.compile("/test.mls")); + +console.log(fs.list); + +console.log(fs.read("/test.mjs")); + From 67727524ec1cbe710a54ae4b63fe9968e6836edc Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:55:36 +0800 Subject: [PATCH 04/22] Remove unnecessary files --- build.sbt | 55 +------------------ .../main/scala/hkmc2/DummyFileSystem.scala | 25 --------- hkmc2/js/src/main/scala/hkmc2/std.scala | 33 ----------- test.mjs | 24 -------- 4 files changed, 2 insertions(+), 135 deletions(-) delete mode 100644 hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala delete mode 100644 hkmc2/js/src/main/scala/hkmc2/std.scala delete mode 100644 test.mjs diff --git a/build.sbt b/build.sbt index 93312ef9f9..c3da720ed5 100644 --- a/build.sbt +++ b/build.sbt @@ -59,62 +59,11 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) .jvmSettings( ) .jsSettings( - Compile / fastOptJS / artifactPath := - baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript-compile"/"apps"/"web-demo"/"build"/"MLscript.mjs", - Compile / fullOptJS / artifactPath := - baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript-compile"/"apps"/"web-demo"/"build"/"MLscript.mjs", scalaJSLinkerConfig ~= { - _.withModuleKind(ModuleKind.ESModule).withMinify(true) - // .withOutputPatterns(OutputPatterns.fromJSFile("MLscript.mjs")) + _.withModuleKind(ModuleKind.ESModule) + .withOutputPatterns(OutputPatterns.fromJSFile("MLscript.mjs")) }, libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.2.0", - // We directly read the necessary MLscript files from the test folders to - // avoid manually embedding the source files in the `WebImporter` class. - Compile / sourceGenerators += Def.task { - def escape(content: String): String = content.iterator.flatMap { - case '\b' => "\\b" case '\t' => "\\t" case '\n' => "\\n" - case '\r' => "\\r" case '\f' => "\\f" case '"' => "\\\"" - case '\\' => "\\\\" case c if c.isControl => f"\\u${c.toInt}%04x" - case c => c.toString - }.mkString("\"", "", "\"") - // Note: baseDirectory.value = ups-web-demo/hkmc2/js - val testFolder = baseDirectory.value / ".." / "shared" / "src" / "test" - val compileFolder = testFolder / "mlscript-compile" - val preludeFile = IO.read(testFolder / "mlscript" / "decls" / "Prelude.mls") - val stdFiles = List("Char", "FingerTreeList", "Iter", "LazyArray", - "LazyFingerTree", "MutMap", "ObjectBuffer", "Option", "Predef", - "Record", "Rendering", "Runtime", "RuntimeJS", "Stack", "StrOps", - "Term", "TreeTracer", "XML") - .iterator - .map { fileName => - (compileFolder / s"$fileName.mls", compileFolder / s"$fileName.mjs") - }.map { case (mlsPath, mjsPath) => - // Only read when the corresponding file exists. - val mls = if (mlsPath.exists()) Some(IO.read(mlsPath)) else None - val mjs = if (mjsPath.exists()) Some(IO.read(mjsPath)) else None - (mlsPath.getName(), mls, mjs) - }.toList - val outFile = (Compile / sourceManaged).value / "generated" / "MLscript.scala" - IO.write( - outFile, - s"""|package hkmc2.generated - |import collection.mutable.Map as MutMap - |object MLscript: - | val preludeFile = ${escape(preludeFile)} - | val sourceFiles: MutMap[String, (mls: Option[String], mjs: Option[String])] = MutMap.empty - |""".stripMargin + - (stdFiles.iterator.map { case (fileName, mlsSourceOpt, mjsSourceOpt) => - val mls = mlsSourceOpt match { - case Some(source) => s"Some(${escape(source)})" - case None => "None" } - val mjs = mjsSourceOpt match { - case Some(source) => s"Some(${escape(source)})" - case None => "None" } - s" sourceFiles += (${escape(s"/std/$fileName")} -> ($mls, $mjs))" - }.mkString("\n")) + "\n" - ) - Seq(outFile) - }.taskValue ) .dependsOn(core) diff --git a/hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala deleted file mode 100644 index c46d7f5abf..0000000000 --- a/hkmc2/js/src/main/scala/hkmc2/DummyFileSystem.scala +++ /dev/null @@ -1,25 +0,0 @@ -package hkmc2 - -import scala.scalajs.js.annotation.* -import scala.scalajs.js, js.JSConverters.* -import mlscript.utils.*, shorthands.* -import io.* - -/** - * Provide a wrapper for virtual file system implemented in JavaScript. - * - * @param module the JavaScript objct representing the file system - */ -@JSExportTopLevel("DummyFileSystem") -class DummyFileSystem(module: js.Dynamic) extends io.FileSystem: - /** Read entire file as string. */ - def read(path: Path): String = - module.read(path.toString).asInstanceOf[String] - - /** Write string to file, overwriting if exists. */ - def write(path: Path, content: String): Unit = - module.write(path.toString, content) - - /** Check if a file exists at the given path. */ - def exists(path: Path): Bool = - module.exists(path.toString).asInstanceOf[Bool] diff --git a/hkmc2/js/src/main/scala/hkmc2/std.scala b/hkmc2/js/src/main/scala/hkmc2/std.scala deleted file mode 100644 index 54bfd8d6fe..0000000000 --- a/hkmc2/js/src/main/scala/hkmc2/std.scala +++ /dev/null @@ -1,33 +0,0 @@ -package hkmc2 - -import io.*, mlscript.utils.*, shorthands.* -import scala.scalajs.js, js.annotation.*, js.JSConverters.* -import scala.collection.mutable.Buffer - -@JSExportTopLevel("std") -object std: - @JSExport - val prelude = generated.MLscript.preludeFile - - @JSExport - val files = - val buffer = Buffer.empty[js.Tuple2[String, String]] - generated.MLscript.sourceFiles.foreach: - case (fileName, (mlsSourceOpt, mjsSourceOpt)) => - mlsSourceOpt match - case Some(mlsSource) => - buffer += js.Tuple2(fileName, mlsSource) - case None => () - mjsSourceOpt match - case Some(mjsSource) => - buffer += js.Tuple2(fileName.dropRight(3) + "mjs", mjsSource) - case None => () - buffer.toJSArray - - @JSExport - def defaultFileSystem: InMemoryFileSystem = - new InMemoryFileSystem(files.map(t => (t._1, t._2)).toMap + ("/Prelude.mls" -> prelude)) - - @JSExport - def defaultPaths: Paths = - new Paths("/Prelude.mls", "/Runtime.mjs", "/Term.mjs") \ No newline at end of file diff --git a/test.mjs b/test.mjs deleted file mode 100644 index 1f23751acf..0000000000 --- a/test.mjs +++ /dev/null @@ -1,24 +0,0 @@ -// Note: This file will be removed before the PR is merged. - -import * as mlscript from "./hkmc2/shared/src/test/mlscript-compile/apps/web-demo/build/MLscript.mjs"; - -const fs = mlscript.std.defaultFileSystem -const paths = mlscript.std.defaultPaths -const compiler = new mlscript.Compiler(fs, paths); - -console.log(Object.getPrototypeOf(fs)); - -const program = ` -class Some[A](x: A) -object None -type Option[A] = Some[A] | None -`; - -fs.write("/test.mls", program); - -console.log(compiler.compile("/test.mls")); - -console.log(fs.list); - -console.log(fs.read("/test.mjs")); - From 39bfa0d7333a6e747aa92e036630a16cf7bc715e Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:40:22 +0800 Subject: [PATCH 05/22] Try to fix the `mkOutput` type --- .../test/scala/hkmc2/CompileTestRunner.scala | 18 ++++++------------ .../scala/hkmc2/utils/ReportFormatter.scala | 12 +++++++++--- .../src/test/scala/hkmc2/DiffMaker.scala | 5 +---- .../src/test/scala/hkmc2/Watcher.scala | 13 +++---------- 4 files changed, 19 insertions(+), 29 deletions(-) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala index 01511a19b1..7a7ef1a0a9 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala @@ -52,19 +52,15 @@ class CompileTestRunner given Config = Config.default.copy(rewriteWhileLoops = false) given io.FileSystem = io.FileSystem.default - // * The weird type of `mkOutput` is to allow wrapping the reporting of - // * diagnostics in synchronized blocks. - // TODO: Fix the weird type, which should be unnecessary in `Watcher`. - val mkOutput = (outputConsumer: (Str => Unit) => Unit) => - // * Synchronize diagnostic output to avoid interleaving since the compiler tests run in parallel - CompileTestRunner.synchronized: - outputConsumer(System.out.println) - val report = ReportFormatter(mkOutput) + val output: Str => Unit = System.out.println + // Synchronize diagnostic output to avoid interleaving since the compiler tests run in parallel. + val wrap: (=> Unit) => Unit = body => CompileTestRunner.synchronized(body) + val report = ReportFormatter(output, Some(wrap)) def mkRaise(file: io.Path): Raise = val wd = file.up - d => mkOutput: + d => wrap: val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) - _(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) + output(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) report(0, d :: Nil, showRelativeLineNums = false) val compiler = MLsCompiler( @@ -85,5 +81,3 @@ class CompileTestRunner end CompileTestRunner object CompileTestRunner - - diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala index 78cc1d2cb5..f0e45ecf69 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala @@ -5,13 +5,17 @@ import collection.mutable import mlscript.utils.*, shorthands.* -class ReportFormatter(mkOutput: ((Str => Unit) => Unit) => Unit): +class ReportFormatter( + output: Str => Unit, + // Allows callers to wrap a whole diagnostic emission (e.g. synchronized). + val wrap: Opt[(=> Unit) => Unit] = N +): val badLines = mutable.Buffer.empty[Int] // report errors and warnings - def apply(blockLineNum: Int, diags: Ls[Diagnostic], showRelativeLineNums: Bool): Unit = mkOutput: output => - diags.foreach { diag => + def apply(blockLineNum: Int, diags: Ls[Diagnostic], showRelativeLineNums: Bool): Unit = + def mk = diags.foreach { diag => val sctx = Message.mkCtx(diag.allMsgs.iterator.map(_._1), "?") val onlyOneLine = diag.allMsgs.size =:= 1 && diag.allMsgs.head._2.isEmpty val headStr = @@ -89,4 +93,6 @@ class ReportFormatter(mkOutput: ((Str => Unit) => Unit) => Unit): () } + + wrap.fold(mk)(_ => mk) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala index 93cbc932d5..833cf69158 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala @@ -141,8 +141,7 @@ abstract class DiffMaker: val strw = new java.io.StringWriter val out = new java.io.PrintWriter(strw) val output = Outputter(out) - val report = ReportFormatter: outputConsumer => - outputConsumer(output(_)) + val report = ReportFormatter(output(_)) val failures = mutable.Buffer.empty[Int] val unmergedChanges = mutable.Buffer.empty[Int] @@ -361,5 +360,3 @@ abstract class DiffMaker: end DiffMaker - - diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index 715d303b27..b0e7ba4b4c 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -99,17 +99,12 @@ class Watcher(dirs: Ls[File]): then given Config = Config.default given FileSystem = FileSystem.default - // * The weird type of `mkOutput` is to allow wrapping the reporting of - // * diagnostics in synchronized blocks. - // TODO: Fix the weird type, which should be unnecessary in `Watcher`. - val mkOutput = (outputConsumer: (Str => Unit) => Unit) => - outputConsumer(System.out.println) - val report = ReportFormatter(mkOutput) + val report = ReportFormatter(System.out.println) def mkRaise(file: io.Path): Raise = val wd = file.up - d => mkOutput: + d => val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) - _(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) + System.out.println(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) report(0, d :: Nil, showRelativeLineNums = false) // Necessary paths used by the compiler. val paths = new MLsCompiler.Paths: @@ -143,5 +138,3 @@ class Watcher(dirs: Ls[File]): def onDelete(file: File, count: Int) = println(pre + show(file).toString + fansi.Color.Blue(" deleted")) // go(file) - - From 95e7d8dc6cdb9f2103f04c1b3c5e48f92e0ab115 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 01:59:30 +0800 Subject: [PATCH 06/22] Very minor improvements on `InMemoryFileSystem` --- hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala index 83878d1ace..4d6ac7e02b 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala @@ -9,20 +9,18 @@ import scala.scalajs.js, js.annotation.JSExport, js.JSConverters.* * from path strings to content strings. Note that separators are not normalized. */ class InMemoryFileSystem(initialFiles: Map[String, String]) extends FileSystem: + // We assume that all paths are normalized here. private val files: MutMap[String, String] = MutMap.from(initialFiles) - def read(path: Path): String = - files.getOrElse(path.toString, throw new FileSystem.FileNotFoundException(path)) + def read(path: Path): String = read(path.toString) def write(path: Path, content: String): Unit = - print(s"Writing to $path") - files(path.toString) = content + write(path.toString, content) def exists(path: Path): Bool = files.contains(path.toString) @JSExport("write") def write(path: Str, content: Str): Unit = - print(s"I'm writing to $path") files(path) = content @JSExport("read") From 52e7f50be42ea45d327bae4e3d71b397447c702d Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 02:05:56 +0800 Subject: [PATCH 07/22] Revert unnecessary whitespace changes --- .../src/test/scala/hkmc2/CompileTestRunner.scala | 2 ++ .../src/main/scala/hkmc2/MLsCompiler.scala | 2 +- .../main/scala/hkmc2/semantics/Importer.scala | 16 ++++++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala index 7a7ef1a0a9..9d4f4b2b74 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala @@ -81,3 +81,5 @@ class CompileTestRunner end CompileTestRunner object CompileTestRunner + + diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index f98d6e10c3..c54b79b605 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -15,7 +15,7 @@ import semantics.Elaborator.{Ctx, State} class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State, raise: Raise, fs: io.FileSystem): - + val block = fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index a96bb30946..4f1a3d2898 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -15,7 +15,7 @@ import hkmc2.syntax.LetBind class Importer: self: Elaborator => import tl.* - + def importPath(path: Str)(using cfg: Config, fs: io.FileSystem): Import = // log(s"pwd: ${os.pwd}") // log(s"wd: ${wd}") @@ -42,15 +42,15 @@ class Importer: Import(sym, file.toString, file) case "mls" => - + val block = fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) - + val sym = tl.trace(s">>> Importing $file"): - + // TODO add parser option to omit internal impls - + val lexer = new syntax.Lexer(origin, dbg = tl.doTrace) val tokens = lexer.bracketedTokens val rules = syntax.ParseRules() @@ -60,15 +60,15 @@ class Importer: if dbg then tl.log(msg) val res = p.parseAll(p.block(allowNewlines = true)) val resBlk = new syntax.Tree.Block(res) - + given Elaborator.Ctx = prelude.copy(mode = Mode.Light).nestLocal("prelude") val elab = Elaborator(tl, file.up, prelude) elab.importFrom(resBlk) - + resBlk.definedSymbols.find(_._1 === nme) match case Some(nme -> sym) => sym case None => lastWords(s"File $file does not define a symbol named $nme") - + val jsFile = file.up / io.RelPath(file.baseName + ".mjs") Import(sym, jsFile.toString, jsFile) From 2d9adb2aa8a6e41697769208edff96bdb61ca869 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 02:13:13 +0800 Subject: [PATCH 08/22] Revert more unnecessary whitespace changes --- hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala | 2 +- hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index 4f1a3d2898..f52eff9a23 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -68,7 +68,7 @@ class Importer: resBlk.definedSymbols.find(_._1 === nme) match case Some(nme -> sym) => sym case None => lastWords(s"File $file does not define a symbol named $nme") - + val jsFile = file.up / io.RelPath(file.baseName + ".mjs") Import(sym, jsFile.toString, jsFile) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index b0e7ba4b4c..feb843d7b2 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -138,3 +138,5 @@ class Watcher(dirs: Ls[File]): def onDelete(file: File, count: Int) = println(pre + show(file).toString + fansi.Color.Blue(" deleted")) // go(file) + + From b73c6cc6328b8fb7d46aac47b9dbef8eb0aff842 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 02:13:53 +0800 Subject: [PATCH 09/22] Revert deleted empty lines --- hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala index 833cf69158..8b199d6af5 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala @@ -360,3 +360,5 @@ abstract class DiffMaker: end DiffMaker + + From 8f97706b25777f291df8845cbfb8745110b2ae66 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 02:21:17 +0800 Subject: [PATCH 10/22] Whitespaces matter --- .../src/main/scala/hkmc2/io/VirtualPath.scala | 5 +- .../hkmc2/io/node/PlatformFileSystem.scala | 4 +- .../scala/hkmc2/io/node/PlatformPath.scala | 22 ++++---- .../scala/hkmc2/io/VirtualPathTests.scala | 52 +++++++++---------- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala index 0cb0709601..422c218816 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala @@ -16,7 +16,7 @@ private[io] class VirtualPath(val pathString: String) extends Path: val isAbs = path.startsWith(sep) // Resolve . and .. segments - val normalized = segments.foldLeft(List.empty[String]) { (acc, seg) => + val normalized = segments.foldLeft(List.empty[String]): (acc, seg) => seg match case "." => acc // Current directory, skip it case ".." => @@ -24,8 +24,7 @@ private[io] class VirtualPath(val pathString: String) extends Path: if acc.isEmpty || acc.last == ".." then acc :+ seg else acc.dropRight(1) case _ => acc :+ seg - } - + if isAbs then sep + normalized.mkString(sep) else if normalized.isEmpty then "." else normalized.mkString(sep) diff --git a/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala index cab9de8bec..2c24619540 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala @@ -22,10 +22,10 @@ private object NodeFs extends js.Object: private class NodeFileSystem extends FileSystem: def read(path: Path): String = NodeFs.readFileSync(path.toString, "utf8") - + def write(path: Path, content: String): Unit = NodeFs.writeFileSync(path.toString, content) - + def exists(path: Path): Bool = NodeFs.existsSync(path.toString) diff --git a/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala index 1b0a414224..9ea60c101e 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformPath.scala @@ -31,32 +31,32 @@ object NodePath extends js.Object: */ private[io] class NodePath(val pathString: String) extends Path: private lazy val parsed = NodePath.parse(pathString) - + override def toString: String = pathString - + def last: String = parsed.base - + def baseName: String = parsed.name - + def ext: String = if parsed.ext.startsWith(".") then parsed.ext.substring(1) else parsed.ext - + def up: Path = new NodePath(NodePath.dirname(pathString)) - + def /(relPath: RelPath): Path = new NodePath(NodePath.join(pathString, relPath.toString)) def /(fragment: String): Path = new NodePath(pathString + NodePath.sep + fragment) - + def relativeTo(base: Path): Opt[RelPath] = try S(new NodeRelPath(NodePath.relative(base.toString, pathString))) catch case _: Exception => N - + def segments: Ls[String] = pathString.split(NodePath.sep).toList.filter(_.nonEmpty) - + def isAbsolute: Bool = NodePath.isAbsolute(pathString) /** @@ -64,10 +64,10 @@ private[io] class NodePath(val pathString: String) extends Path: */ private[io] class NodeRelPath(val pathString: String) extends RelPath: override def toString: String = pathString - + def segments: Ls[String] = pathString.split(NodePath.sep).toList.filter(_.nonEmpty) - + def /(other: RelPath): RelPath = new NodeRelPath(NodePath.join(pathString, other.toString)) diff --git a/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala b/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala index a9a3d902b8..d9e5b5355e 100644 --- a/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala +++ b/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala @@ -4,127 +4,127 @@ import org.scalatest.funsuite.AnyFunSuite import mlscript.utils._, shorthands._ class VirtualPathTests extends AnyFunSuite: - + test("basic path creation and toString"): val path = VirtualPath("foo/bar") assert(path.toString == "foo/bar") - + test("/ operator with simple fragment"): val path = VirtualPath("foo") val result = path / "bar" assert(result.toString == "foo/bar") - + test("/ operator with fragment starting with ."): val path = VirtualPath("foo") val result = path / "./bar" assert(result.toString == "foo/bar", "Current directory '.' should be removed") - + test("/ operator with fragment starting with ./"): val path = VirtualPath("foo/baz") val result = path / "./bar" assert(result.toString == "foo/baz/bar", "Current directory '.' should be removed") - + test("/ operator with fragment containing .."): val path = VirtualPath("foo/baz") val result = path / "../bar" assert(result.toString == "foo/bar", "Parent directory '..' should navigate up one level") - + test("/ operator with multiple .. segments"): val path = VirtualPath("a/b/c") val result = path / "../../d" assert(result.toString == "a/d", "Multiple '..' should navigate up multiple levels") - + test("/ operator with . in the middle of path"): val path = VirtualPath("foo") val result = path / "bar/./baz" assert(result.toString == "foo/bar/baz", "Current directory '.' in middle should be removed") - + test("/ operator with .. in the middle of path"): val path = VirtualPath("foo") val result = path / "bar/../baz" assert(result.toString == "foo/baz", "Parent directory '..' in middle should collapse segments") - + test("/ operator with absolute path"): val path = VirtualPath("/abs/path") val result = path / "./file.txt" assert(result.toString == "/abs/path/file.txt", "Should work with absolute paths") - + test("/ operator with RelPath containing ."): val path = VirtualPath("foo") val relPath = VirtualRelPath("./bar") val result = path / relPath assert(result.toString == "foo/bar", "RelPath with '.' should be normalized") - + test("/ operator with RelPath containing .."): val path = VirtualPath("foo/baz") val relPath = VirtualRelPath("../bar") val result = path / relPath assert(result.toString == "foo/bar", "RelPath with '..' should be normalized") - + test("/ operator with RelPath containing multiple . and .."): val path = VirtualPath("a/b") val relPath = VirtualRelPath("./c/../d") val result = path / relPath assert(result.toString == "a/b/d", "Complex RelPath should be normalized") - + test("normalization with too many .. segments"): val path = VirtualPath("foo") val result = path / "../../bar" assert(result.toString == "../bar", "Extra '..' should be preserved for relative paths") - + test("normalization resulting in just ."): val path = VirtualPath("foo") val result = path / ".." assert(result.toString == ".", "Navigating up from single segment should result in '.'") - + test("/ operator with trailing slash"): val path = VirtualPath("foo/") val result = path / "bar" assert(result.toString == "foo/bar", "Should handle trailing slash correctly") - + test("path segments"): val path = VirtualPath("foo/bar/baz") assert(path.segments == List("foo", "bar", "baz")) - + test("last segment"): val path = VirtualPath("foo/bar/baz.txt") assert(path.last == "baz.txt") - + test("baseName"): val path = VirtualPath("foo/bar/baz.txt") assert(path.baseName == "baz") - + test("ext"): val path = VirtualPath("foo/bar/baz.txt") assert(path.ext == "txt") - + test("up"): val path = VirtualPath("foo/bar/baz") assert(path.up.toString == "foo/bar") - + test("isAbsolute - relative path"): val path = VirtualPath("foo/bar") assert(!path.isAbsolute) - + test("isAbsolute - absolute path"): val path = VirtualPath("/foo/bar") assert(path.isAbsolute) - + test("complex normalization case"): val path = VirtualPath("a/b/c") val result = path / "./d/../e/./f" assert(result.toString == "a/b/c/e/f", "Complex path with mixed . and .. should normalize correctly") - + test("normalization with only ."): val path = VirtualPath("foo") val result = path / "." assert(result.toString == "foo", "Single '.' should result in same path") - + test("normalization preserves absolute paths"): val path = VirtualPath("/a/b") val result = path / "../c" assert(result.toString == "/a/c", "Absolute paths should remain absolute after normalization") - + test("RelPath / operator"): val rel1 = VirtualRelPath("foo/bar") val rel2 = VirtualRelPath("baz") From c1b31ffde838c5fd620d6efa4e58f74d4b85a1a7 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 02:22:11 +0800 Subject: [PATCH 11/22] Whitespaces matter 2.0 --- hkmc2/shared/src/main/scala/hkmc2/io/Path.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala index fe9a693251..d3b90f8cfa 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala @@ -41,7 +41,7 @@ abstract class Path: object Path: /** Create path from string - delegates to platform-specific implementation */ def apply(str: String): Path = PathFactory.fromString(str) - + /** Platform-specific path separator */ def separator: String = PathFactory.separator @@ -56,6 +56,6 @@ abstract class RelPath: object RelPath: /** Create relative path from string - delegates to platform-specific implementation */ def apply(str: String): RelPath = PathFactory.relPathFromString(str) - + /** Represents parent directory (..) */ val up: RelPath = PathFactory.relPathUp From 2e373989c2badd6d761586e4aa607ccd4064a547 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:07:42 +0800 Subject: [PATCH 12/22] Use `iterator` Co-authored-by: Lionel Parreaux --- hkmc2/js/src/main/scala/hkmc2/Compiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala index 374a69169c..a1b6648ba2 100644 --- a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala +++ b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala @@ -28,7 +28,7 @@ class Compiler(fs: FileSystem, paths: MLsCompiler.Paths): private val compiler = MLsCompiler(paths, mkRaise) private def collectDiagnostics(): js.Array[js.Dynamic] = - pathDiagnosticsMap.iterator.toArray.sortBy(_._2._1).map: + pathDiagnosticsMap.toArray.sortBy(_._2._1).map: case (path, (_, diagnostics)) => js.Dynamic.literal( path = path, diagnostics = diagnostics.map: d => From 605055fbaf990232d8c72b8a2ba5228efe84ee58 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:07:53 +0800 Subject: [PATCH 13/22] Use `iterator` Co-authored-by: Lionel Parreaux --- hkmc2/js/src/main/scala/hkmc2/Compiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala index a1b6648ba2..f1370d4b8c 100644 --- a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala +++ b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala @@ -31,7 +31,7 @@ class Compiler(fs: FileSystem, paths: MLsCompiler.Paths): pathDiagnosticsMap.toArray.sortBy(_._2._1).map: case (path, (_, diagnostics)) => js.Dynamic.literal( path = path, - diagnostics = diagnostics.map: d => + diagnostics = diagnostics.iterator.map: d => js.Dynamic.literal( kind = d.kind.toString().toLowerCase(), source = d.source.toString().toLowerCase(), From 235fac36baa81f553e492bba91e5ca267fd72d1e Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:09:12 +0800 Subject: [PATCH 14/22] Use `iterator` Co-authored-by: Lionel Parreaux --- hkmc2/js/src/main/scala/hkmc2/Compiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala index f1370d4b8c..4dff9f5786 100644 --- a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala +++ b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala @@ -36,7 +36,7 @@ class Compiler(fs: FileSystem, paths: MLsCompiler.Paths): kind = d.kind.toString().toLowerCase(), source = d.source.toString().toLowerCase(), mainMessage = d.theMsg, - allMessages = d.allMsgs.map: + allMessages = d.allMsgs.iterator.map: case (message, loc) => lazy val ctx = ShowCtx.mk: message.bits.collect: From ec404f6631d7c2e935a01e791e3bda0a4d9d47c2 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:07:23 +0800 Subject: [PATCH 15/22] Use the default `Config` in web compiler --- hkmc2/js/src/main/scala/hkmc2/Compiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala index 4dff9f5786..f7f5691c59 100644 --- a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala +++ b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala @@ -16,7 +16,7 @@ import scala.collection.mutable.{ArrayBuffer, Buffer} @JSExportTopLevel("Compiler") class Compiler(fs: FileSystem, paths: MLsCompiler.Paths): - private given Config = Config.default.copy(rewriteWhileLoops = false) + private given Config = Config.default private given FileSystem = fs From b21057116ecb843be9c81708b0ec395b88c95b5d Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:02:06 +0800 Subject: [PATCH 16/22] Fix synchronized output and polish the interface --- .../test/scala/hkmc2/CompileTestRunner.scala | 17 ++----- .../scala/hkmc2/utils/ReportFormatter.scala | 50 +++++++++++++------ .../src/test/scala/hkmc2/DiffMaker.scala | 2 +- .../src/test/scala/hkmc2/Watcher.scala | 20 +++----- 4 files changed, 49 insertions(+), 40 deletions(-) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala index 9d4f4b2b74..feaf7f76a5 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala @@ -45,30 +45,23 @@ class CompileTestRunner test(relativeName): - println(s"Compiling: $relativeName") + CompileTestRunner.synchronized: + println(s"Compiling: $relativeName") // Stack safety relies on the fact that runtime uses while loops for resumption // and does not create extra stack depth. Hence we disable while loop rewriting here. given Config = Config.default.copy(rewriteWhileLoops = false) given io.FileSystem = io.FileSystem.default - val output: Str => Unit = System.out.println // Synchronize diagnostic output to avoid interleaving since the compiler tests run in parallel. val wrap: (=> Unit) => Unit = body => CompileTestRunner.synchronized(body) - val report = ReportFormatter(output, Some(wrap)) - def mkRaise(file: io.Path): Raise = - val wd = file.up - d => wrap: - val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) - output(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) - report(0, d :: Nil, showRelativeLineNums = false) - + val report = ReportFormatter(System.out.println, colorize = true, wrap = Some(wrap)) val compiler = MLsCompiler( - new MLsCompiler.Paths: + paths = new MLsCompiler.Paths: val preludeFile = mainTestDir / "mlscript" / "decls" / "Prelude.mls" val runtimeFile = mainTestDir / "mlscript-compile" / "Runtime.mjs" val termFile = mainTestDir / "mlscript-compile" / "Term.mjs", - mkRaise + mkRaise = report.mkRaise ) compiler.compileModule(file) diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala index f0e45ecf69..6a747180e4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala @@ -5,17 +5,41 @@ import collection.mutable import mlscript.utils.*, shorthands.* +/** + * Formats diagnostic reports using box drawing characters and/or ANSI colors. + * + * @param output The output function (e.g., `System.out.println`). + * @param colorize Whether to colorize the output using ANSI colors. + * @param wrap Optionally wraps each `Raise` call, e.g., with `synchronized`. + */ class ReportFormatter( output: Str => Unit, - // Allows callers to wrap a whole diagnostic emission (e.g. synchronized). + val colorize: Bool, val wrap: Opt[(=> Unit) => Unit] = N ): + /** Output main text. */ + private def text(str: Str) = + output(if colorize then fansi.Color.Red(str).toString else str) + + /** Output title text. */ + private def title(str: Str) = + output(if colorize then fansi.Color.LightRed(str).toString else str) val badLines = mutable.Buffer.empty[Int] - // report errors and warnings + /** Create a `Raise` dedicated to reporting diagnostics for `file`. */ + def mkRaise(file: io.Path): Raise = + // This shows the parent directory of a file and its name. + val relPath = file.relativeTo(file.up.up).map(_.toString).getOrElse(file.toString) + d => + def mk = + title(s"/!!!\\ Error in $relPath /!!!\\") + apply(0, d :: Nil, showRelativeLineNums = false) + wrap.fold(mk)(_(mk)) + + /** Report errors and warnings. */ def apply(blockLineNum: Int, diags: Ls[Diagnostic], showRelativeLineNums: Bool): Unit = - def mk = diags.foreach { diag => + diags.foreach { diag => val sctx = Message.mkCtx(diag.allMsgs.iterator.map(_._1), "?") val onlyOneLine = diag.allMsgs.size =:= 1 && diag.allMsgs.head._2.isEmpty val headStr = @@ -42,10 +66,10 @@ class ReportFormatter( diag.allMsgs.zipWithIndex.foreach { case ((msg, loco), msgNum) => val isLast = msgNum =:= lastMsgNum val msgStr = msg.showIn(using sctx) - if msgNum =:= 0 then output(headStr + msgStr) + if msgNum =:= 0 then text(headStr + msgStr) else if loco.isEmpty && diag.allMsgs.size =:= 1 then - if !onlyOneLine then output("╙──") - else output(s"${if isLast && loco.isEmpty then "╙──" else "╟──"} ${msgStr}") + if !onlyOneLine then text("╙──") + else text(s"${if isLast && loco.isEmpty then "╙──" else "╟──"} ${msgStr}") loco.foreach { loc => val (startLineNum, startLineStr, startLineCol) = loc.origin.fph.getLineColAt(loc.spanStart) @@ -64,7 +88,7 @@ class ReportFormatter( val prepre = "║ " val pre = s"$shownLineNum: " val curLine = loc.origin.fph.lines(l - 1) - output(prepre + pre + "\t" + curLine) + text(prepre + pre + "\t" + curLine) val tickBuilder = new StringBuilder() tickBuilder ++= ( (if isLast && l =:= endLineNum then "╙──" else prepre) @@ -72,27 +96,25 @@ class ReportFormatter( val lastCol = if l =:= endLineNum then endLineCol else curLine.length + 1 while c < lastCol do { tickBuilder += ('^'); c += 1 } if c =:= startLineCol then tickBuilder += ('^') - output(tickBuilder.toString) + text(tickBuilder.toString) c = 1 l += 1 } } - if diag.allMsgs.isEmpty then output("╙──") + if diag.allMsgs.isEmpty then text("╙──") // if (!mode.fixme) { // if (!allowTypeErrors // && !mode.expectTypeErrors && diag.isInstanceOf[ErrorReport] && diag.source =:= Diagnostic.Typing) - // { output("TEST CASE FAILURE: There was an unexpected type error"); failures += globalLineNum } + // { text("TEST CASE FAILURE: There was an unexpected type error"); failures += globalLineNum } // if (!allowParseErrors // && !mode.expectParseErrors && diag.isInstanceOf[ErrorReport] && (diag.source =:= Diagnostic.Lexing || diag.source =:= Diagnostic.Parsing)) - // { output("TEST CASE FAILURE: There was an unexpected parse error"); failures += globalLineNum } + // { text("TEST CASE FAILURE: There was an unexpected parse error"); failures += globalLineNum } // if (!allowTypeErrors && !allowParseErrors // && !mode.expectWarnings && diag.isInstanceOf[WarningReport]) - // { output("TEST CASE FAILURE: There was an unexpected warning"); failures += globalLineNum } + // { text("TEST CASE FAILURE: There was an unexpected warning"); failures += globalLineNum } // } () } - - wrap.fold(mk)(_ => mk) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala index 8b199d6af5..32dc0dd121 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala @@ -141,7 +141,7 @@ abstract class DiffMaker: val strw = new java.io.StringWriter val out = new java.io.PrintWriter(strw) val output = Outputter(out) - val report = ReportFormatter(output(_)) + val report = ReportFormatter(output(_), colorize = false) val failures = mutable.Buffer.empty[Int] val unmergedChanges = mutable.Buffer.empty[Int] diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index feb843d7b2..fcc291f2e2 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -99,19 +99,13 @@ class Watcher(dirs: Ls[File]): then given Config = Config.default given FileSystem = FileSystem.default - val report = ReportFormatter(System.out.println) - def mkRaise(file: io.Path): Raise = - val wd = file.up - d => - val relPath = file.relativeTo(wd.up).map(_.toString).getOrElse(file.toString) - System.out.println(fansi.Color.LightRed(s"/!!!\\ Error in $relPath /!!!\\").toString) - report(0, d :: Nil, showRelativeLineNums = false) - // Necessary paths used by the compiler. - val paths = new MLsCompiler.Paths: - val preludeFile = preludePath - val runtimeFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Runtime.mjs" - val termFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Term.mjs" - MLsCompiler(paths, mkRaise).compileModule(path) + MLsCompiler( + paths = new MLsCompiler.Paths: + val preludeFile = preludePath + val runtimeFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Runtime.mjs" + val termFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Term.mjs", + mkRaise = ReportFormatter(System.out.println, colorize = false).mkRaise + ).compileModule(path) else val dm = new MainDiffMaker(rootPath.toString, path, preludePath, predefPath, relativeName): override def fs = FileSystem.default From 2ab93413ec3dcce502ee6c6f1967e8788ae203dc Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:44:53 +0800 Subject: [PATCH 17/22] Add a simple script for testing the web compiler --- bin/test-compile.mjs | 92 +++++++++++++++++++ .../scala/hkmc2/io/InMemoryFileSystem.scala | 7 ++ 2 files changed, 99 insertions(+) create mode 100644 bin/test-compile.mjs diff --git a/bin/test-compile.mjs b/bin/test-compile.mjs new file mode 100644 index 0000000000..4d1acd31b9 --- /dev/null +++ b/bin/test-compile.mjs @@ -0,0 +1,92 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { inspect } from "node:util"; + +// Update the relative path here if this file is moved. +const projectPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +async function loadStandardLibrary() { + const compilePath = path.join(projectPath, "hkmc2/shared/src/test/mlscript-compile"); + const files = await Promise.all( + (await fs.readdir(compilePath)) + .filter((fileName) => { + const ext = path.extname(fileName); + return ext === ".mls" || ext === ".mjs"; + }) + .map(async (fileName) => ({ + path: `/std/${fileName}`, + content: await fs.readFile(path.join(compilePath, fileName), "utf-8"), + })) + ); + const preludePath = path.join(projectPath, "hkmc2/shared/src/test/mlscript/decls/Prelude.mls"); + files.push({ path: "/std/Prelude.mls", content: await fs.readFile(preludePath, "utf-8") }); + return files; +} + +async function importMLscript() { + // Check if "./hkmc2/js/target/scala-3.7.3/hkmc2-opt/MLscript.mjs" exists. + // If not, check "./hkmc2/js/target/scala-3.7.3/hkmc2-fastopt/MLscript.mjs" + const mlscriptPath = path.join(projectPath, "hkmc2/js/target/scala-3.7.3/hkmc2-opt/MLscript.mjs"); + const mlscriptFastOptPath = path.join(projectPath, "hkmc2/js/target/scala-3.7.3/hkmc2-fastopt/MLscript.mjs"); + try { + await fs.access(mlscriptPath); + return await import(mlscriptPath); + } catch { + try { + await fs.access(mlscriptFastOptPath); + return await import(mlscriptFastOptPath); + } catch { + throw new Error( + `MLscript module not found. Please build the project first.` + ); + } + } +} + +async function main() { + const { Compiler, InMemoryFileSystem, Paths } = await importMLscript(); + const fileSystem = InMemoryFileSystem( + (await loadStandardLibrary()).map(({ path, content }) => [path, content]) + ); + const paths = new Paths( + "/std/Prelude.mls", + "/std/Runtime.mjs", + "/std/Term.mjs" + ); + const compiler = new Compiler(fileSystem, paths); + const program = `import "./std/Option.mls" +import "./std/Stack.mls" +import "./std/Predef.mls" + +open Stack +open Option +open Predef + +fun findFirst(xs, f) = if xs is + Nil then None + Cons(x, xs') and + f(x) then Some(x) + else findFirst(xs', f) + +let nums = 1 :: 2 :: 3 :: 4 :: 5 :: Nil +let result = nums \\findFirst of x => x * 6 is 24 +`; + console.log(bold(red("Source Program:"))); + console.log(program); + fileSystem.write("/test.mls", program); + console.log(bold(red("Dianostics:"))); + console.log(inspect(compiler.compile("/test.mls"), { depth: null })); + console.log(bold(red("Compiled JavaScript:"))); + console.log(fileSystem.read("/test.mjs")); +} + +main(); + +function red(text) { + return `\x1b[31m${text}\x1b[0m`; +} + +function bold(text) { + return `\x1b[1m${text}\x1b[0m`; +} diff --git a/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala index 4d6ac7e02b..48b05723e0 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala @@ -3,6 +3,7 @@ package hkmc2.io import mlscript.utils._, shorthands._ import collection.mutable.Map as MutMap import scala.scalajs.js, js.annotation.JSExport, js.JSConverters.* +import scala.scalajs.js.annotation.JSExportTopLevel /** * In-memory file system for testing and web compiler. Stores files as a map @@ -32,3 +33,9 @@ class InMemoryFileSystem(initialFiles: Map[String, String]) extends FileSystem: /** Get all files (for debugging) */ def allFiles: Map[String, String] = files.toMap + +object InMemoryFileSystem: + /** Create an empty in-memory file system. */ + @JSExportTopLevel("InMemoryFileSystem") + def apply(files: js.Array[js.Tuple2[Str, Str]]): InMemoryFileSystem = + new InMemoryFileSystem(files.map(t => t._1 -> t._2).toMap) From 6281d5d8a1d5274653d147755f44d82e00bbf3be Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:04:21 +0800 Subject: [PATCH 18/22] Organize `import`s Co-authored-by: Lionel Parreaux --- hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index fcc291f2e2..e619e1eea9 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -6,10 +6,10 @@ import scala.jdk.CollectionConverters.* import mlscript.utils.*, shorthands.* import better.files.* -import _root_.io.methvin.better.files.* -import _root_.io.methvin.watcher.{DirectoryWatcher, PathUtils} -import _root_.io.methvin.watcher.{DirectoryChangeEvent, DirectoryChangeListener} -import _root_.io.methvin.watcher.hashing.{FileHash, FileHasher} +import _root_.io.methvin +import methvin.better.files.* +import methvin.watcher.{DirectoryWatcher, PathUtils, DirectoryChangeEvent, DirectoryChangeListener} +import methvin.watcher.hashing.{FileHash, FileHasher} import java.time.LocalDateTime import java.time.temporal._ import io.FileSystem, io.PlatformPath.given From 2d0ee908c9c8dbae2b10d16c9ae3baa19cae7c88 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:06:55 +0800 Subject: [PATCH 19/22] Watcher should have turned on `colorize` option Co-authored-by: Lionel Parreaux --- hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index e619e1eea9..d07e442544 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -104,7 +104,7 @@ class Watcher(dirs: Ls[File]): val preludeFile = preludePath val runtimeFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Runtime.mjs" val termFile = rootPath/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Term.mjs", - mkRaise = ReportFormatter(System.out.println, colorize = false).mkRaise + mkRaise = ReportFormatter(System.out.println, colorize = true).mkRaise ).compileModule(path) else val dm = new MainDiffMaker(rootPath.toString, path, preludePath, predefPath, relativeName): From 4ebd7291a3787b26618b24d421e0dad5fdcdffe4 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:28:38 +0800 Subject: [PATCH 20/22] Use `Vector` instead of `List` for `children` and `subTerms` --- .../src/main/scala/hkmc2/codegen/Block.scala | 22 +-- .../main/scala/hkmc2/semantics/Pattern.scala | 56 ++++---- .../scala/hkmc2/semantics/SimpleSplit.scala | 30 ++-- .../main/scala/hkmc2/semantics/Split.scala | 22 +-- .../src/main/scala/hkmc2/semantics/Term.scala | 134 +++++++++--------- .../hkmc2/semantics/ucs/FlatPattern.scala | 19 +-- .../scala/hkmc2/semantics/ups/Pattern.scala | 26 ++-- .../shared/src/main/scala/hkmc2/syntax.scala | 2 +- .../src/main/scala/hkmc2/syntax/Tree.scala | 88 ++++++------ 9 files changed, 202 insertions(+), 197 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index d3aa3a5908..3b855ec6d8 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -527,17 +527,17 @@ sealed abstract class Result extends AutoLocated: // * for the location to be valid, we should NOT have it include children whose location // * is from some different place (with a different Origin), such as the location attached to symbols. // * That's why for example, we're not adding the `l` of `Value.Ref` to the children list. - protected def children: List[Located] = this match - case Call(fun, args) => fun :: args.map(_.value) - case Instantiate(mut, cls, args) => cls :: args.map(_.value) - case Select(qual, name) => qual :: name :: Nil - case DynSelect(qual, fld, arrayIdx) => qual :: fld :: Nil - case Lambda(params, body) => params :: Nil - case Tuple(mut, elems) => elems.map(_.value) - case Record(mut, elems) => elems.map(_.value) - case Value.Ref(l, disamb) => Nil - case Value.This(sym) => Nil - case Value.Lit(lit) => lit :: Nil + protected def children: Vector[Located] = this match + case Call(fun, args) => fun +: args.iterator.map(_.value).toVector + case Instantiate(mut, cls, args) => cls +: args.iterator.map(_.value).toVector + case Select(qual, name) => Vector(qual, name) + case DynSelect(qual, fld, arrayIdx) => Vector(qual, fld) + case Lambda(params, body) => Vector(params) + case Tuple(mut, elems) => elems.iterator.map(_.value).toVector + case Record(mut, elems) => elems.iterator.map(_.value).toVector + case Value.Ref(l, disamb) => Vector.empty + case Value.This(sym) => Vector.empty + case Value.Lit(lit) => Vector(lit) // TODO rm Lam from values and thus the need for this method def subBlocks: Ls[Block] = this match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala index cd1c29ffe3..de80651709 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala @@ -281,39 +281,41 @@ enum Pattern extends AutoLocated: case Annotated(pattern, _) => pattern.variables case Guarded(pattern, _) => pattern.variables - def children: Ls[Located] = this match - case Constructor(target, arguments) => target :: arguments.getOrElse(Nil) - case Composition(polarity, left, right) => left :: right :: Nil - case Negation(pattern) => pattern :: Nil - case Wildcard() => Nil - case Literal(literal) => literal :: Nil - case Range(lower, upper, rightInclusive) => lower :: upper :: Nil - case Concatenation(left, right) => left :: right :: Nil - case Tuple(leading, spread) => leading ::: spread.fold(Nil): - case (_, middle, trailing) => middle :: trailing - case Record(fields) => fields.flatMap: - case (name, pattern) => name :: pattern.children - case Chain(first, second) => first :: second :: Nil - case Alias(pattern, alias) => pattern :: alias :: Nil - case Transform(pattern, _, transform) => pattern :: transform :: Nil - case Annotated(pattern, annotations) => pattern :: - annotations.iterator.collect { case R(term) => term }.toList + def children: Vector[Located] = this match + case Constructor(target, arguments) => target +: arguments.fold(Vector.empty)(_.toVector) + case Composition(polarity, left, right) => Vector(left, right) + case Negation(pattern) => Vector(pattern) + case Wildcard() => Vector.empty + case Literal(literal) => Vector(literal) + case Range(lower, upper, rightInclusive) => Vector(lower, upper) + case Concatenation(left, right) => Vector(left, right) + case Tuple(leading, spread) => leading.toVector ++ spread.fold(Vector.empty): + case (_, middle, trailing) => middle +: trailing.toVector + case Record(fields) => + fields.iterator.flatMap: + case (name, pattern) => name +: pattern.children + .toVector + case Chain(first, second) => Vector(first, second) + case Alias(pattern, alias) => Vector(pattern, alias) + case Transform(pattern, _, transform) => Vector(pattern, transform) + case Annotated(pattern, annotations) => pattern +: + annotations.iterator.collect { case R(term) => term }.toVector case Guarded(pattern, guard) => pattern.children :+ guard - def subTerms: Ls[Term] = this match + def subTerms: Vector[Term] = this match case Constructor(target, arguments) => - target :: arguments.fold(Nil)(_.flatMap(_.subTerms)) - case Composition(_, left, right) => left.subTerms ::: right.subTerms + target +: Vector.concat(arguments.fold(Vector.empty)(_.iterator.flatMap(_.subTerms).toVector)) + case Composition(_, left, right) => left.subTerms ++ right.subTerms case Negation(pattern) => pattern.subTerms - case _: (Wildcard | Literal | Range) => Nil - case Concatenation(left, right) => left.subTerms ::: right.subTerms - case Tuple(leading, spread) => leading.flatMap(_.subTerms) ::: spread.fold(Nil): - case (_, middle, trailing) => middle.subTerms ::: trailing.flatMap(_.subTerms) - case Record(fields) => fields.flatMap(_._2.subTerms) - case Chain(first, second) => first.subTerms ::: second.subTerms + case _: (Wildcard | Literal | Range) => Vector.empty + case Concatenation(left, right) => left.subTerms ++ right.subTerms + case Tuple(leading, spread) => leading.iterator.flatMap(_.subTerms).toVector ++ spread.fold(Vector.empty): + case (_, middle, trailing) => middle.subTerms ++ trailing.iterator.flatMap(_.subTerms).toVector + case Record(fields) => fields.iterator.flatMap(_._2.subTerms).toVector + case Chain(first, second) => first.subTerms ++ second.subTerms case Alias(pattern, _) => pattern.subTerms case Transform(pattern, _, transform) => pattern.subTerms :+ transform - case Annotated(pattern, annotations) => pattern.subTerms ::: + case Annotated(pattern, annotations) => pattern.subTerms ++ annotations.iterator.collect { case R(term) => term }.toList case Guarded(pattern, guard) => pattern.subTerms :+ guard diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala index 42d25ca5a2..ee3de1d9a1 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala @@ -29,17 +29,17 @@ enum SimpleSplit extends AutoLocated with ProductWithTail: case els: Else => els case End => this - protected def children: List[Located] = this match - case Cons(branch, tail) => List(branch, tail) + protected def children: Vector[Located] = this match + case Cons(branch, tail) => Vector(branch, tail) case els @ Else(default) => els.kw match - case N => default :: Nil - case S(kw) => kw :: default :: Nil - case End => Nil + case N => Vector(default) + case S(kw) => Vector(kw, default) + case End => Vector.empty - def subTerms: Ls[Term] = this match - case Cons(branch, tail) => branch.subTerms ::: tail.subTerms - case Else(default) => default :: Nil - case End => Nil + def subTerms: Vector[Term] = this match + case Cons(branch, tail) => branch.subTerms.toVector ++ tail.subTerms + case Else(default) => Vector(default) + case End => Vector.empty def showDbg: Str = this match case Cons(branch, tail) => s"${branch.showDbg}; ${tail.showDbg}" @@ -84,10 +84,10 @@ object SimpleSplit: case Match(scrutinee: Term.Ref, pattern: Pattern, consequent: SimpleSplit) case Let(binding: BlockLocalSymbol, term: Term) - def subTerms: Ls[Term] = this match + def subTerms: Vector[Term] = this match case Match(scrutinee, pattern, consequent) => - scrutinee :: pattern.subTerms ::: consequent.subTerms - case Let(_, term) => term :: Nil + scrutinee +: (pattern.subTerms ++ consequent.subTerms) + case Let(_, term) => Vector(term) def showDbg: Str = this match case Match(scrutinee, pattern, consequent) => @@ -98,10 +98,10 @@ object SimpleSplit: s"${scrutinee.showDbg} is ${pattern.showDbg} ${consequentStr}" case Let(binding, term) => s"let ${binding.nme} = ${term.showDbg}" - protected def children: List[Located] = this match + protected def children: Vector[Located] = this match case Match(scrutinee, pattern, consequent) => - List(scrutinee, pattern, consequent) - case Let(binding, term) => List(binding, term) + Vector(scrutinee, pattern, consequent) + case Let(binding, term) => Vector(binding, term) private[semantics] object prettyPrint: /** Represents lines with indentations. */ diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala index 695e40453b..4aa4621bf5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala @@ -10,7 +10,7 @@ final case class Branch(scrutinee: Term.Ref, pattern: FlatPattern, continuation: (Tree.Ident(scrutinee.tree.name), scrutinee.refNum, scrutinee.typ) Branch(scrutineeClone, pattern.mkClone, continuation.mkClone) - override def children: List[Located] = scrutinee :: pattern :: continuation :: Nil + override def children: Vector[Located] = Vector(scrutinee, pattern, continuation) def showDbg: String = s"${scrutinee.sym.nme} is ${pattern.showDbg} -> { ${continuation.showDbg} }" @@ -60,18 +60,18 @@ enum Split extends AutoLocated with ProductWithTail: case Split.Else(_) | Split.Cons(_, _) => false case Split.End => true - final override def children: Ls[Located] = this match - case Split.Cons(head, tail) => List(head, tail) - case Split.Let(name, term, tail) => List(name, term, tail) - case Split.Else(default) => List(default) - case Split.End => Nil + final override def children: Vector[Located] = this match + case Split.Cons(head, tail) => Vector(head, tail) + case Split.Let(name, term, tail) => Vector(name, term, tail) + case Split.Else(default) => Vector(default) + case Split.End => Vector.empty - def subTerms: Ls[Term] = this match + def subTerms: Vector[Term] = this match case Split.Cons(Branch(scrutinee, pattern, continuation), tail) => - scrutinee :: pattern.subTerms ++ continuation.subTerms ++ tail.subTerms - case Split.Let(_, term, tail) => term :: tail.subTerms - case Split.Else(term) => term :: Nil - case Split.End => Nil + scrutinee +: (pattern.subTerms ++ continuation.subTerms ++ tail.subTerms) + case Split.Let(_, term, tail) => term +: tail.subTerms + case Split.Else(term) => Vector(term) + case Split.End => Vector.empty final def showDbg: String = this match case Split.Cons(head, tail) => s"${head.showDbg}; ${tail.showDbg}" diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 072158100f..071e1393e3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -25,13 +25,13 @@ enum Annot extends AutoLocated: case Trm(trm) => trm.symbol case _ => N - def subTerms: Ls[Term] = this match - case Trm(trm) => trm :: Nil - case _: Modifier | Untyped | TailRec | TailCall => Nil + def subTerms: Vector[Term] = this match + case Trm(trm) => Vector(trm) + case _: Modifier | Untyped | TailRec | TailCall => Vector.empty - def children: Ls[Located] = this match - case Trm(trm) => trm :: Nil - case _: Modifier | Untyped | TailRec | TailCall => Nil + def children: Vector[Located] = this match + case Trm(trm) => Vector(trm) + case _: Modifier | Untyped | TailRec | TailCall => Vector.empty def mkClone(using State): Annot = this match case Untyped => Untyped @@ -454,62 +454,62 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: case r: SelProj => r.symbol.mkString case _ => "" - def subStatements: Ls[Statement] = this match - case Blk(stats, res) => stats ::: res :: Nil + def subStatements: Vector[Statement] = this match + case Blk(stats, res) => stats.toVector :+ res case _ => subTerms - def subTerms: Ls[Term] = this match - case Error | Missing | _: Lit | _: Ref | _: UnitVal => Nil - case Resolved(t, sym) => t :: Nil - case App(lhs, rhs) => lhs :: rhs :: Nil - case RcdField(lhs, rhs) => lhs :: rhs :: Nil - case RcdSpread(bod) => bod :: Nil - case FunTy(lhs, rhs, eff) => lhs :: rhs :: eff.toList - case TyApp(pre, tarsg) => pre :: tarsg - case Sel(pre, _) => pre :: Nil - case SynthSel(pre, _) => pre :: Nil - case DynSel(o, f, _) => o :: f :: Nil - case Tup(fields) => fields.flatMap(_.subTerms) - case Mut(und) => und :: Nil - case CtxTup(fields) => fields.flatMap(_.subTerms) + def subTerms: Vector[Term] = this match + case Error | Missing | _: Lit | _: Ref | _: UnitVal => Vector.empty + case Resolved(t, sym) => Vector(t) + case App(lhs, rhs) => Vector(lhs, rhs) + case RcdField(lhs, rhs) => Vector(lhs, rhs) + case RcdSpread(bod) => Vector(bod) + case FunTy(lhs, rhs, eff) => Vector(lhs, rhs) ++ eff.toVector + case TyApp(pre, tarsg) => pre +: tarsg.toVector + case Sel(pre, _) => Vector(pre) + case SynthSel(pre, _) => Vector(pre) + case DynSel(o, f, _) => Vector(o, f) + case Tup(fields) => fields.flatMap(_.subTerms).toVector + case Mut(und) => Vector(und) + case CtxTup(fields) => fields.flatMap(_.subTerms).toVector case IfLike(_, split) => split.subTerms case SynthIf(split) => split.subTerms - case Lam(params, body) => body :: Nil - case Blk(stats, res) => stats.flatMap(_.subTerms) ::: res :: Nil - case Rcd(mut, stats) => stats.flatMap(_.subTerms) - case Quoted(term) => term :: Nil - case Unquoted(term) => term :: Nil - case New(cls, args, rft) => cls :: args ::: rft.toList.flatMap(_._2.blk.subTerms) - case DynNew(cls, args) => cls :: args - case SelProj(pre, cls, _) => pre :: cls :: Nil - case Asc(term, ty) => term :: ty :: Nil - case Ret(res) => res :: Nil - case Throw(res) => res :: Nil - case Forall(_, _, body) => body :: Nil - case WildcardTy(in, out) => in.toList ++ out.toList - case CompType(lhs, rhs, _) => lhs :: rhs :: Nil - case LetDecl(sym, annotations) => annotations.flatMap(_.subTerms) - case DefineVar(sym, rhs) => rhs :: Nil - case Region(_, body) => body :: Nil - case RegRef(reg, value) => reg :: value :: Nil - case Assgn(lhs, rhs) => lhs :: rhs :: Nil - case SetRef(lhs, rhs) => lhs :: rhs :: Nil - case Drop(term) => term :: Nil - case Deref(term) => term :: Nil + case Lam(params, body) => Vector(body) + case Blk(stats, res) => stats.flatMap(_.subTerms).toVector :+ res + case Rcd(mut, stats) => stats.flatMap(_.subTerms).toVector + case Quoted(term) => Vector(term) + case Unquoted(term) => Vector(term) + case New(cls, args, rft) => (cls +: args.toVector) ++ rft.toVector.flatMap(_._2.blk.subTerms) + case DynNew(cls, args) => cls +: args.toVector + case SelProj(pre, cls, _) => Vector(pre, cls) + case Asc(term, ty) => Vector(term, ty) + case Ret(res) => Vector(res) + case Throw(res) => Vector(res) + case Forall(_, _, body) => Vector(body) + case WildcardTy(in, out) => in.toVector ++ out.toVector + case CompType(lhs, rhs, _) => Vector(lhs, rhs) + case LetDecl(sym, annotations) => annotations.flatMap(_.subTerms).toVector + case DefineVar(sym, rhs) => Vector(rhs) + case Region(_, body) => Vector(body) + case RegRef(reg, value) => Vector(reg, value) + case Assgn(lhs, rhs) => Vector(lhs, rhs) + case SetRef(lhs, rhs) => Vector(lhs, rhs) + case Drop(term) => Vector(term) + case Deref(term) => Vector(term) case TermDefinition(_, _, _, pss, tps, sign, body, res, _, _, annotations, _) => - pss.toList.flatMap(_.subTerms) ::: tps.getOrElse(Nil).flatMap(_.subTerms) ::: sign.toList ::: body.toList ::: annotations.flatMap(_.subTerms) + pss.toVector.flatMap(_.subTerms) ++ tps.getOrElse(Nil).flatMap(_.subTerms).toVector ++ sign.toVector ++ body.toVector ++ annotations.flatMap(_.subTerms).toVector case cls: ClassDef => - cls.paramsOpt.toList.flatMap(_.subTerms) ::: cls.body.blk :: cls.annotations.flatMap(_.subTerms) + (cls.paramsOpt.toVector.flatMap(_.subTerms) :+ cls.body.blk) ++ cls.annotations.flatMap(_.subTerms).toVector case mod: ModuleOrObjectDef => - mod.paramsOpt.toList.flatMap(_.subTerms) ::: mod.body.blk :: mod.annotations.flatMap(_.subTerms) + ( mod.paramsOpt.toVector.flatMap(_.subTerms) :+ mod.body.blk) ++ mod.annotations.flatMap(_.subTerms).toVector case td: TypeDef => - td.rhs.toList ::: td.annotations.flatMap(_.subTerms) + td.rhs.toVector ++ td.annotations.flatMap(_.subTerms).toVector case pat: PatternDef => - pat.paramsOpt.toList.flatMap(_.subTerms) ::: pat.body.blk :: pat.annotations.flatMap(_.subTerms) - case Import(sym, str, pth) => Nil - case Try(body, finallyDo) => body :: finallyDo :: Nil - case Handle(lhs, rhs, args, derivedClsSym, defs, bod) => rhs :: args ::: defs.flatMap(_.td.subTerms) ::: bod :: Nil - case Neg(e) => e :: Nil - case Annotated(ann, target) => ann.subTerms ::: target :: Nil + (pat.paramsOpt.toVector.flatMap(_.subTerms) :+ pat.body.blk) ++ pat.annotations.flatMap(_.subTerms).toVector + case Import(sym, str, pth) => Vector.empty + case Try(body, finallyDo) => Vector(body, finallyDo) + case Handle(lhs, rhs, args, derivedClsSym, defs, bod) => (rhs +: args.toVector) ++ defs.flatMap(_.td.subTerms).toVector :+ bod + case Neg(e) => Vector(e) + case Annotated(ann, target) => ann.subTerms ++ Vector(target) /** Check if the term satisfies the predicate. The reason I did not use * `subTerms` and `subStatements` for traversal is that they consume a @@ -587,21 +587,21 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: go(this) // private def treeOrSubterms(t: Tree, t: Term): Ls[Located] = t match - private def treeOrSubterms(t: Tree): Ls[Located] = t match + private def treeOrSubterms(t: Tree): Vector[Located] = t match case Tree.DummyApp | Tree.DummyTup => subTerms - case _ => t :: Nil + case _ => Vector(t) - protected def children: Ls[Located] = this match - case t: Lit => t.lit.asTree :: Nil + protected def children: Vector[Located] = this match + case t: Lit => Vector(t.lit.asTree) case t: Ref => treeOrSubterms(t.tree) case t: Tup => treeOrSubterms(t.tree) - case l: Lam => l.params.paramSyms.map(_.id) ::: l.body :: Nil + case l: Lam => l.params.paramSyms.map(_.id).toVector :+ l.body case t: App => treeOrSubterms(t.tree) - case IfLike(_, split) => split :: Nil - case SynthIf(split) => split :: Nil - case SynthSel(pre, nme) => pre :: nme :: Nil - case Sel(pre, nme) => pre :: nme :: Nil - case SelProj(prefix, cls, proj) => prefix :: cls :: proj :: Nil + case IfLike(_, split) => Vector(split) + case SynthIf(split) => Vector(split) + case SynthSel(pre, nme) => Vector(pre, nme) + case Sel(pre, nme) => Vector(pre, nme) + case SelProj(prefix, cls, proj) => Vector(prefix, cls, proj) case _ => subTerms // TODO more precise (include located things that aren't terms) @@ -1061,12 +1061,12 @@ final case class Param(flags: FldFlags, sym: VarSymbol, sign: Opt[Term], modulef extends Declaration, AutoLocated: var fldSym: Opt[MemberSymbol] = N def subTerms: Ls[Term] = sign.toList - override protected def children: List[Located] = sym :: sign.toList + override protected def children: Vector[Located] = sym +: sign.toVector def showDbg: Str = flags.show + sym + sign.fold("")(": " + _.showDbg) final case class ParamList(flags: ParamListFlags, params: Ls[Param], restParam: Opt[Param]) extends AutoLocated: - override protected def children: List[Located] = params ::: restParam.toList + override protected def children: Vector[Located] = params.toVector ++ restParam.toVector def foreach(f: Param => Unit): Unit = (params ++ restParam).foreach(f) def paramCountLB: Int = params.length @@ -1093,7 +1093,7 @@ object ParamListFlags: trait FldImpl extends AutoLocated: self: Fld => - def children: Ls[Located] = self.term :: self.asc.toList ::: Nil + def children: Vector[Located] = self.term +: self.asc.toVector def showDbg: Str = flags.show + self.term.showDbg def describe: Str = (if self.flags.spec then "specialized " else "") + diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala index fdf22f192e..722ec09f5a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala @@ -44,15 +44,18 @@ enum FlatPattern extends AutoLocated: case Tuple(size, inf) => Tuple(size, inf) case Record(entries) => Record(entries) - def subTerms: Ls[Term] = this match - case p: ClassLike => p.constructor :: Nil - case _: (Lit | Tuple | Record) => Nil + def subTerms: Vector[Term] = this match + case p: ClassLike => Vector(p.constructor) + case _: (Lit | Tuple | Record) => Vector.empty - def children: Ls[Located] = this match - case Lit(literal) => literal :: Nil - case ClassLike(ctor, symbol, scruts, _) => ctor :: scruts.fold(Nil)(_.map(_._1)) - case Tuple(fields, _) => Nil - case Record(entries) => entries.flatMap { case (nme, als) => nme :: als :: Nil } + def children: Vector[Located] = this match + case Lit(literal) => Vector(literal) + case ClassLike(ctor, symbol, scruts, _) => Vector(ctor) ++ scruts.fold(Vector.empty)(_.map(_._1)) + case Tuple(fields, _) => Vector.empty + case Record(entries) => + entries.iterator.flatMap: + case (nme, als) => Vector(nme, als) + .toVector def showDbg: Str = (this match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala index 7e9c8324f6..403b459d43 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala @@ -48,19 +48,19 @@ sealed abstract class Pattern[+K <: Kind.Complete] extends AutoLocated: case _ => Or[L](this :: that :: Nil) // TODO: Associate with locations. - protected def children: List[Located] = this match - case Literal(lit) => lit :: Nil - case ClassLike(sym, arguments) => arguments.fold(Nil): - _.map((id, p) => p).toList - case Record(entries) => entries.values.toList - case Tuple(leading, spread) => leading ::: spread.fold(Nil): - case (_, middle, trailing) => middle :: trailing - case And(patterns) => patterns - case Or(patterns) => patterns - case Not(pattern) => pattern :: Nil - case Rename(pattern, name) => pattern :: Nil - case Extract(pattern, _, term) => pattern :: term :: Nil - case Synonym(pattern) => pattern.symbol :: pattern.arguments + protected def children: Vector[Located] = this match + case Literal(lit) => Vector(lit) + case ClassLike(sym, arguments) => arguments.fold(Vector.empty): + _.map((id, p) => p).toVector + case Record(entries) => entries.values.toVector + case Tuple(leading, spread) => leading.toVector ++ spread.fold(Vector.empty): + case (_, middle, trailing) => middle +: trailing.toVector + case And(patterns) => patterns.toVector + case Or(patterns) => patterns.toVector + case Not(pattern) => Vector(pattern) + case Rename(pattern, name) => Vector(pattern) + case Extract(pattern, _, term) => Vector(pattern, term) + case Synonym(pattern) => pattern.symbol +: pattern.arguments.toVector lazy val symbols: Ls[VarSymbol] = this match case Literal(lit) => Nil diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax.scala index 40f421946b..968457a313 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax.scala @@ -24,7 +24,7 @@ trait Located: def toLoc: Opt[Loc] trait AutoLocated extends Located: - protected def children: List[Located] + protected def children: Vector[Located] private var loc: Opt[Loc] = N diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala index 2f23305bd9..e5c731af9f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala @@ -119,51 +119,51 @@ enum Tree extends AutoLocated: case _: (Ident | Literal | Error) => acc case _ => die - def children: Ls[Located] = this match - case _: Empty | _: Error | _: Ident | _: Literal | _: Under | _: Unt => Nil - case Pun(_, e) => e :: Nil - case Bra(_, e) => e :: Nil - case Block(stmts) => stmts - case LetLike(kw, lhs, rhs, body) => lhs :: Nil ++ rhs ++ body + def children: Vector[Located] = this match + case _: Empty | _: Error | _: Ident | _: Literal | _: Under | _: Unt => Vector.empty + case Pun(_, e) => Vector(e) + case Bra(_, e) => Vector(e) + case Block(stmts) => stmts.toVector + case LetLike(kw, lhs, rhs, body) => lhs +: (rhs.toVector ++ body.toVector) case Hndl(lhs, rhs, defs, body) => body match - case Some(value) => lhs :: rhs :: defs :: value :: Nil - case None => lhs :: rhs :: defs :: Nil - case TypeDef(k, head, rhs) => head :: rhs.toList - case Modified(_, body) => Ls(body) - case Quoted(body) => Ls(body) - case Unquoted(body) => Ls(body) - case Tup(fields) => fields - case App(lhs, rhs) => Ls(lhs, rhs) - case OpApp(lhs, op, rhss) => lhs :: op :: rhss - case Jux(lhs, rhs) => Ls(lhs, rhs) - case PrefixApp(kw, rhs) => kw :: rhs :: Nil - case InfixApp(lhs, kw, rhs) => lhs :: kw :: rhs :: Nil - case TermDef(k, head, rhs) => head :: rhs.toList - case LexicalNew(body, rft) => body.toList ::: rft.toList - case ProperNew(body, rft) => body.toList ::: rft.toList - case DynamicNew(body) => body :: Nil - case IfLike(_, split) => split :: Nil - case Case(_, bs) => Ls(bs) - case Region(name, body) => name :: body :: Nil - case RegRef(reg, value) => reg :: value :: Nil - case Effectful(eff, body) => eff :: body :: Nil - case Outer(name) => name.toList - case TyTup(tys) => tys - case Sel(prefix, name) => prefix :: Nil - case SynthSel(prefix, name) => prefix :: Nil - case DynAccess(prefix, fld) => prefix :: fld :: Nil - case Open(bod) => bod :: Nil - case OpenIn(opened, body) => opened :: body :: Nil - case Def(lhs, rhs) => lhs :: rhs :: Nil - case Spread(kw, body) => kw :: body.toList - case Annotated(annotation, target) => annotation :: target :: Nil - case Constructor(decl) => decl :: Nil - case MemberProj(cls, name) => cls :: Nil - case Keywrd(kw) => Nil - case Dummy => Nil - case OpSplit(lhs, ops_rhss) => lhs :: ops_rhss - case SplitPoint() => Nil - case Trm(trm) => trm :: Nil + case Some(value) => lhs +: rhs +: defs +: value +: Vector.empty + case None => lhs +: rhs +: defs +: Vector.empty + case TypeDef(k, head, rhs) => head +: rhs.toVector + case Modified(_, body) => Vector(body) + case Quoted(body) => Vector(body) + case Unquoted(body) => Vector(body) + case Tup(fields) => fields.toVector + case App(lhs, rhs) => Vector(lhs, rhs) + case OpApp(lhs, op, rhss) => lhs +: op +: rhss.toVector + case Jux(lhs, rhs) => Vector(lhs, rhs) + case PrefixApp(kw, rhs) => Vector(kw, rhs) + case InfixApp(lhs, kw, rhs) => Vector(lhs, kw, rhs) + case TermDef(k, head, rhs) => head +: rhs.toVector + case LexicalNew(body, rft) => body.toVector ++ rft.toVector + case ProperNew(body, rft) => body.toVector ++ rft.toVector + case DynamicNew(body) => Vector(body) + case IfLike(_, split) => Vector(split) + case Case(_, bs) => Vector(bs) + case Region(name, body) => Vector(name, body) + case RegRef(reg, value) => Vector(reg, value) + case Effectful(eff, body) => Vector(eff, body) + case Outer(name) => name.toVector + case TyTup(tys) => tys.toVector + case Sel(prefix, name) => Vector(prefix) + case SynthSel(prefix, name) => Vector(prefix) + case DynAccess(prefix, fld) => Vector(prefix, fld) + case Open(bod) => Vector(bod) + case OpenIn(opened, body) => Vector(opened, body) + case Def(lhs, rhs) => Vector(lhs, rhs) + case Spread(kw, body) => Vector(kw) ++ body.toVector + case Annotated(annotation, target) => Vector(annotation, target) + case Constructor(decl) => Vector(decl) + case MemberProj(cls, name) => Vector(cls) + case Keywrd(kw) => Vector.empty + case Dummy => Vector.empty + case OpSplit(lhs, ops_rhss) => lhs +: ops_rhss.toVector + case SplitPoint() => Vector.empty + case Trm(trm) => Vector(trm) def describe: Str = this match case Empty() => "empty" From 6739160d5cb48c09b5ac62aead01c8c88603d0d3 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:23:14 +0800 Subject: [PATCH 21/22] Remove `exists` and traverse `subTerms` directly --- .../src/main/scala/hkmc2/MLsCompiler.scala | 4 +- .../src/main/scala/hkmc2/semantics/Term.scala | 75 ------------------- 2 files changed, 3 insertions(+), 76 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index c54b79b605..89bcc51312 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -88,9 +88,11 @@ class MLsCompiler(paths: MLsCompiler.Paths, mkRaise: io.Path => Raise)(using con val (blk0, _) = elab.importFrom(parsed) val resolver = Resolver(rtl) resolver.traverseBlock(blk0)(using Resolver.ICtx.empty) - val hasQuote = blk0.exists: + def findQuote(t: semantics.Statement): Bool = t match case Term.Quoted(_) | Term.Unquoted(_) => true case Term.Ref(sym) => sym === State.termSymbol + case _ => t.subTerms.exists(findQuote) + val hasQuote = findQuote(blk0) val blk = new Term.Blk( Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: // Only import `Term.mls` when necessary. diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 071e1393e3..f6a1d8caa1 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -511,81 +511,6 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: case Neg(e) => Vector(e) case Annotated(ann, target) => ann.subTerms ++ Vector(target) - /** Check if the term satisfies the predicate. The reason I did not use - * `subTerms` and `subStatements` for traversal is that they consume a - * considerable amount of time and memory, and they fail to terminate on - * `Rule.mls`. */ - def exists(p: PartialFunction[Statement, Bool]): Bool = - def go(stmt: Statement): Bool = - stmt match - case _ if p.isDefinedAt(stmt) => p(stmt) - case Error | _: Lit | _: UnitVal | Missing | _: Ref => false - case App(lhs, rhs) => go(lhs) || go(rhs) - case RcdField(lhs, rhs) => go(lhs) || go(rhs) - case RcdSpread(bod) => go(bod) - case FunTy(lhs, rhs, eff) => go(lhs) || go(rhs) || eff.exists(go) - case TyApp(pre, tarsg) => go(pre) || tarsg.exists(go) - case Sel(pre, _) => go(pre) - case SynthSel(pre, _) => go(pre) - case DynSel(o, f, _) => go(o) || go(f) - case Tup(fields) => fields.exists(_.subTerms.exists(go)) - case IfLike(_, body) => body.subTerms.exists(go) - case Lam(params, body) => params.params.exists(_.subTerms.exists(go)) || go(body) - case Blk(stats, res) => stats.exists(go) || go(res) - case Rcd(mut, stats) => stats.exists(go) - case Quoted(term) => go(term) - case Unquoted(term) => go(term) - case New(cls, argss, rft) => - go(cls) || argss.exists(_.exists(go(_))) || - rft.exists(_._2.blk.subTerms.exists(go)) - case SelProj(pre, cls, _) => go(pre) || go(cls) - case Asc(term, ty) => go(term) || go(ty) - case Ret(res) => go(res) - case Throw(res) => go(res) - case Forall(_, _, body) => go(body) - case WildcardTy(in, out) => in.exists(go) || out.exists(go) - case CompType(lhs, rhs, _) => go(lhs) || go(rhs) - case LetDecl(sym, annotations) => annotations.flatMap(_.subTerms).exists(go) - case DefineVar(sym, rhs) => go(rhs) - case Region(_, body) => go(body) - case RegRef(reg, value) => go(reg) || go(value) - case Assgn(lhs, rhs) => go(lhs) || go(rhs) - case SetRef(lhs, rhs) => go(lhs) || go(rhs) - case Deref(term) => go(term) - case TermDefinition(_, _, _, pss, tps, sign, body, _, _, _, annotations, _) => - pss.toList.flatMap(_.subTerms).exists(go) || tps.getOrElse(Nil).exists: - case Param(_, _, sign, _) => sign.exists(go) - || sign.toList.exists(go) || body.exists(go) || annotations.exists: - case Annot.Trm(term) => go(term) - case Annot.Untyped | Annot.Modifier(_) => false - case cls: ClassDef => - cls.paramsOpt.toList.flatMap(_.subTerms).exists(go) || - go(cls.body.blk) || - cls.annotations.flatMap(_.subTerms).exists(go) - case mod: ModuleOrObjectDef => - mod.paramsOpt.toList.flatMap(_.subTerms).exists(go) || - go(mod.body.blk) || - mod.annotations.flatMap(_.subTerms).exists(go) - case td: TypeDef => - td.rhs.toList.exists(go) || td.annotations.flatMap(_.subTerms).exists(go) - case pat: PatternDef => - pat.paramsOpt.toList.flatMap(_.subTerms).exists(go) || - go(pat.body.blk) || - pat.annotations.flatMap(_.subTerms).exists(go) - case Import(sym, str, file) => false - case Try(body, finallyDo) => go(body) && go(finallyDo) - case Handle(lhs, rhs, args, derivedClsSym, defs, bod) => - go(rhs) || args.exists(go) || defs.flatMap(_.td.subTerms).exists(go) || go(bod) - case Neg(e) => go(e) - case Annotated(ann, target) => ann.subTerms.exists(go) || go(target) - case Mut(underlying) => go(underlying) - case DynNew(cls, args) => go(cls) && args.exists(go) - case Resolved(t, sym) => go(t) - case CtxTup(fields) => fields.exists(_.subTerms.exists(go)) - case SynthIf(split) => split.subTerms.exists(go) - case Drop(trm) => go(trm) - go(this) - // private def treeOrSubterms(t: Tree, t: Term): Ls[Located] = t match private def treeOrSubterms(t: Tree): Vector[Located] = t match case Tree.DummyApp | Tree.DummyTup => subTerms From 41f47ef0d88fc5f8906aef2073fcbb7e667353a6 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sat, 13 Dec 2025 16:50:38 +0800 Subject: [PATCH 22/22] Avoid the use of varargs when constructing vectors --- core/shared/main/scala/utils/package.scala | 9 ++- .../src/main/scala/hkmc2/codegen/Block.scala | 8 +- .../main/scala/hkmc2/semantics/Pattern.scala | 18 ++--- .../scala/hkmc2/semantics/SimpleSplit.scala | 14 ++-- .../main/scala/hkmc2/semantics/Split.scala | 10 +-- .../src/main/scala/hkmc2/semantics/Term.scala | 76 +++++++++---------- .../scala/hkmc2/semantics/ups/Pattern.scala | 8 +- .../src/main/scala/hkmc2/syntax/Tree.scala | 52 ++++++------- 8 files changed, 101 insertions(+), 94 deletions(-) diff --git a/core/shared/main/scala/utils/package.scala b/core/shared/main/scala/utils/package.scala index 85291d1e94..fb3e7b9168 100644 --- a/core/shared/main/scala/utils/package.scala +++ b/core/shared/main/scala/utils/package.scala @@ -206,10 +206,12 @@ package object utils { } } + // * The goal of these is to avoid the use of varargs, which I've found to be a source of + // * overhead in the past, due to the allocation of intermediate arrays. + // * Remains to be seen if using these is always (or ever?) necessarily a win. implicit class MutSetObjectHelpers(self: mutable.Set.type) { def single[A](a: A): mutable.Set[A] = mutable.Set.empty[A] += a } - implicit class SetObjectHelpers(self: Set.type) { def single[A](a: A): Set[A] = (Set.newBuilder[A] += a).result() } @@ -222,6 +224,11 @@ package object utils { implicit class SortedMapObjectHelpers(self: SortedMap.type) { def single[A: Ordering, B](ab: A -> B): SortedMap[A, B] = (SortedMap.newBuilder[A, B] += ab).result() } + implicit class VectorObjectHelpers(self: Vector.type) { + def single[A](a: A): Vector[A] = a +: Vector.empty + def double[A](a: A, b: A): Vector[A] = a +: b +: Vector.empty + def triple[A](a: A, b: A, c: A): Vector[A] = a +: b +: c +: Vector.empty + } def TODO(msg: Any): Nothing = throw new NotImplementedError( msg.toString + s" (of class ${msg.getClass().getSimpleName()})") diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index 3b855ec6d8..879737680c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -530,14 +530,14 @@ sealed abstract class Result extends AutoLocated: protected def children: Vector[Located] = this match case Call(fun, args) => fun +: args.iterator.map(_.value).toVector case Instantiate(mut, cls, args) => cls +: args.iterator.map(_.value).toVector - case Select(qual, name) => Vector(qual, name) - case DynSelect(qual, fld, arrayIdx) => Vector(qual, fld) - case Lambda(params, body) => Vector(params) + case Select(qual, name) => Vector.double(qual, name) + case DynSelect(qual, fld, arrayIdx) => Vector.double(qual, fld) + case Lambda(params, body) => Vector.single(params) case Tuple(mut, elems) => elems.iterator.map(_.value).toVector case Record(mut, elems) => elems.iterator.map(_.value).toVector case Value.Ref(l, disamb) => Vector.empty case Value.This(sym) => Vector.empty - case Value.Lit(lit) => Vector(lit) + case Value.Lit(lit) => Vector.single(lit) // TODO rm Lam from values and thus the need for this method def subBlocks: Ls[Block] = this match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala index de80651709..1956970544 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala @@ -254,7 +254,7 @@ enum Pattern extends AutoLocated: this match case Annotated(pattern, annotations) => Annotated(pattern, annotations :+ elem) - case _ => Annotated(this, Vector(elem)) + case _ => Annotated(this, Vector.single(elem)) inline def withGuard(guard: Term) = Pattern.Guarded(this, guard) @@ -283,21 +283,21 @@ enum Pattern extends AutoLocated: def children: Vector[Located] = this match case Constructor(target, arguments) => target +: arguments.fold(Vector.empty)(_.toVector) - case Composition(polarity, left, right) => Vector(left, right) - case Negation(pattern) => Vector(pattern) + case Composition(polarity, left, right) => Vector.double(left, right) + case Negation(pattern) => Vector.single(pattern) case Wildcard() => Vector.empty - case Literal(literal) => Vector(literal) - case Range(lower, upper, rightInclusive) => Vector(lower, upper) - case Concatenation(left, right) => Vector(left, right) + case Literal(literal) => Vector.single(literal) + case Range(lower, upper, rightInclusive) => Vector.double(lower, upper) + case Concatenation(left, right) => Vector.double(left, right) case Tuple(leading, spread) => leading.toVector ++ spread.fold(Vector.empty): case (_, middle, trailing) => middle +: trailing.toVector case Record(fields) => fields.iterator.flatMap: case (name, pattern) => name +: pattern.children .toVector - case Chain(first, second) => Vector(first, second) - case Alias(pattern, alias) => Vector(pattern, alias) - case Transform(pattern, _, transform) => Vector(pattern, transform) + case Chain(first, second) => Vector.double(first, second) + case Alias(pattern, alias) => Vector.double(pattern, alias) + case Transform(pattern, _, transform) => Vector.double(pattern, transform) case Annotated(pattern, annotations) => pattern +: annotations.iterator.collect { case R(term) => term }.toVector case Guarded(pattern, guard) => pattern.children :+ guard diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala index ee3de1d9a1..9b8be228a5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala @@ -30,15 +30,15 @@ enum SimpleSplit extends AutoLocated with ProductWithTail: case End => this protected def children: Vector[Located] = this match - case Cons(branch, tail) => Vector(branch, tail) + case Cons(branch, tail) => Vector.double(branch, tail) case els @ Else(default) => els.kw match - case N => Vector(default) - case S(kw) => Vector(kw, default) + case N => Vector.single(default) + case S(kw) => Vector.double(kw, default) case End => Vector.empty def subTerms: Vector[Term] = this match case Cons(branch, tail) => branch.subTerms.toVector ++ tail.subTerms - case Else(default) => Vector(default) + case Else(default) => Vector.single(default) case End => Vector.empty def showDbg: Str = this match @@ -87,7 +87,7 @@ object SimpleSplit: def subTerms: Vector[Term] = this match case Match(scrutinee, pattern, consequent) => scrutinee +: (pattern.subTerms ++ consequent.subTerms) - case Let(_, term) => Vector(term) + case Let(_, term) => Vector.single(term) def showDbg: Str = this match case Match(scrutinee, pattern, consequent) => @@ -100,8 +100,8 @@ object SimpleSplit: protected def children: Vector[Located] = this match case Match(scrutinee, pattern, consequent) => - Vector(scrutinee, pattern, consequent) - case Let(binding, term) => Vector(binding, term) + Vector.triple(scrutinee, pattern, consequent) + case Let(binding, term) => Vector.double(binding, term) private[semantics] object prettyPrint: /** Represents lines with indentations. */ diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala index 4aa4621bf5..89671785f3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala @@ -10,7 +10,7 @@ final case class Branch(scrutinee: Term.Ref, pattern: FlatPattern, continuation: (Tree.Ident(scrutinee.tree.name), scrutinee.refNum, scrutinee.typ) Branch(scrutineeClone, pattern.mkClone, continuation.mkClone) - override def children: Vector[Located] = Vector(scrutinee, pattern, continuation) + override def children: Vector[Located] = Vector.triple(scrutinee, pattern, continuation) def showDbg: String = s"${scrutinee.sym.nme} is ${pattern.showDbg} -> { ${continuation.showDbg} }" @@ -61,16 +61,16 @@ enum Split extends AutoLocated with ProductWithTail: case Split.End => true final override def children: Vector[Located] = this match - case Split.Cons(head, tail) => Vector(head, tail) - case Split.Let(name, term, tail) => Vector(name, term, tail) - case Split.Else(default) => Vector(default) + case Split.Cons(head, tail) => Vector.double(head, tail) + case Split.Let(name, term, tail) => Vector.triple(name, term, tail) + case Split.Else(default) => Vector.single(default) case Split.End => Vector.empty def subTerms: Vector[Term] = this match case Split.Cons(Branch(scrutinee, pattern, continuation), tail) => scrutinee +: (pattern.subTerms ++ continuation.subTerms ++ tail.subTerms) case Split.Let(_, term, tail) => term +: tail.subTerms - case Split.Else(term) => Vector(term) + case Split.Else(term) => Vector.single(term) case Split.End => Vector.empty final def showDbg: String = this match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index f6a1d8caa1..875c3a2b8b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -26,11 +26,11 @@ enum Annot extends AutoLocated: case _ => N def subTerms: Vector[Term] = this match - case Trm(trm) => Vector(trm) + case Trm(trm) => Vector.single(trm) case _: Modifier | Untyped | TailRec | TailCall => Vector.empty def children: Vector[Located] = this match - case Trm(trm) => Vector(trm) + case Trm(trm) => Vector.single(trm) case _: Modifier | Untyped | TailRec | TailCall => Vector.empty def mkClone(using State): Annot = this match @@ -459,74 +459,74 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: case _ => subTerms def subTerms: Vector[Term] = this match case Error | Missing | _: Lit | _: Ref | _: UnitVal => Vector.empty - case Resolved(t, sym) => Vector(t) - case App(lhs, rhs) => Vector(lhs, rhs) - case RcdField(lhs, rhs) => Vector(lhs, rhs) - case RcdSpread(bod) => Vector(bod) - case FunTy(lhs, rhs, eff) => Vector(lhs, rhs) ++ eff.toVector + case Resolved(t, sym) => Vector.single(t) + case App(lhs, rhs) => Vector.double(lhs, rhs) + case RcdField(lhs, rhs) => Vector.double(lhs, rhs) + case RcdSpread(bod) => Vector.single(bod) + case FunTy(lhs, rhs, eff) => Vector.double(lhs, rhs) ++ eff.toVector case TyApp(pre, tarsg) => pre +: tarsg.toVector - case Sel(pre, _) => Vector(pre) - case SynthSel(pre, _) => Vector(pre) - case DynSel(o, f, _) => Vector(o, f) + case Sel(pre, _) => Vector.single(pre) + case SynthSel(pre, _) => Vector.single(pre) + case DynSel(o, f, _) => Vector.double(o, f) case Tup(fields) => fields.flatMap(_.subTerms).toVector - case Mut(und) => Vector(und) + case Mut(und) => Vector.single(und) case CtxTup(fields) => fields.flatMap(_.subTerms).toVector case IfLike(_, split) => split.subTerms case SynthIf(split) => split.subTerms - case Lam(params, body) => Vector(body) + case Lam(params, body) => Vector.single(body) case Blk(stats, res) => stats.flatMap(_.subTerms).toVector :+ res case Rcd(mut, stats) => stats.flatMap(_.subTerms).toVector - case Quoted(term) => Vector(term) - case Unquoted(term) => Vector(term) + case Quoted(term) => Vector.single(term) + case Unquoted(term) => Vector.single(term) case New(cls, args, rft) => (cls +: args.toVector) ++ rft.toVector.flatMap(_._2.blk.subTerms) case DynNew(cls, args) => cls +: args.toVector - case SelProj(pre, cls, _) => Vector(pre, cls) - case Asc(term, ty) => Vector(term, ty) - case Ret(res) => Vector(res) - case Throw(res) => Vector(res) - case Forall(_, _, body) => Vector(body) + case SelProj(pre, cls, _) => Vector.double(pre, cls) + case Asc(term, ty) => Vector.double(term, ty) + case Ret(res) => Vector.single(res) + case Throw(res) => Vector.single(res) + case Forall(_, _, body) => Vector.single(body) case WildcardTy(in, out) => in.toVector ++ out.toVector - case CompType(lhs, rhs, _) => Vector(lhs, rhs) + case CompType(lhs, rhs, _) => Vector.double(lhs, rhs) case LetDecl(sym, annotations) => annotations.flatMap(_.subTerms).toVector - case DefineVar(sym, rhs) => Vector(rhs) - case Region(_, body) => Vector(body) - case RegRef(reg, value) => Vector(reg, value) - case Assgn(lhs, rhs) => Vector(lhs, rhs) - case SetRef(lhs, rhs) => Vector(lhs, rhs) - case Drop(term) => Vector(term) - case Deref(term) => Vector(term) + case DefineVar(sym, rhs) => Vector.single(rhs) + case Region(_, body) => Vector.single(body) + case RegRef(reg, value) => Vector.double(reg, value) + case Assgn(lhs, rhs) => Vector.double(lhs, rhs) + case SetRef(lhs, rhs) => Vector.double(lhs, rhs) + case Drop(term) => Vector.single(term) + case Deref(term) => Vector.single(term) case TermDefinition(_, _, _, pss, tps, sign, body, res, _, _, annotations, _) => pss.toVector.flatMap(_.subTerms) ++ tps.getOrElse(Nil).flatMap(_.subTerms).toVector ++ sign.toVector ++ body.toVector ++ annotations.flatMap(_.subTerms).toVector case cls: ClassDef => (cls.paramsOpt.toVector.flatMap(_.subTerms) :+ cls.body.blk) ++ cls.annotations.flatMap(_.subTerms).toVector case mod: ModuleOrObjectDef => - ( mod.paramsOpt.toVector.flatMap(_.subTerms) :+ mod.body.blk) ++ mod.annotations.flatMap(_.subTerms).toVector + ( mod.paramsOpt.toVector.flatMap(_.subTerms) :+ mod.body.blk) ++ mod.annotations.flatMap(_.subTerms).toVector case td: TypeDef => td.rhs.toVector ++ td.annotations.flatMap(_.subTerms).toVector case pat: PatternDef => (pat.paramsOpt.toVector.flatMap(_.subTerms) :+ pat.body.blk) ++ pat.annotations.flatMap(_.subTerms).toVector case Import(sym, str, pth) => Vector.empty - case Try(body, finallyDo) => Vector(body, finallyDo) + case Try(body, finallyDo) => Vector.single(body) ++ Vector.single(finallyDo) case Handle(lhs, rhs, args, derivedClsSym, defs, bod) => (rhs +: args.toVector) ++ defs.flatMap(_.td.subTerms).toVector :+ bod - case Neg(e) => Vector(e) - case Annotated(ann, target) => ann.subTerms ++ Vector(target) + case Neg(e) => Vector.single(e) + case Annotated(ann, target) => ann.subTerms ++ Vector.single(target) // private def treeOrSubterms(t: Tree, t: Term): Ls[Located] = t match private def treeOrSubterms(t: Tree): Vector[Located] = t match case Tree.DummyApp | Tree.DummyTup => subTerms - case _ => Vector(t) + case _ => Vector.single(t) protected def children: Vector[Located] = this match - case t: Lit => Vector(t.lit.asTree) + case t: Lit => Vector.single(t.lit.asTree) case t: Ref => treeOrSubterms(t.tree) case t: Tup => treeOrSubterms(t.tree) case l: Lam => l.params.paramSyms.map(_.id).toVector :+ l.body case t: App => treeOrSubterms(t.tree) - case IfLike(_, split) => Vector(split) - case SynthIf(split) => Vector(split) - case SynthSel(pre, nme) => Vector(pre, nme) - case Sel(pre, nme) => Vector(pre, nme) - case SelProj(prefix, cls, proj) => Vector(prefix, cls, proj) + case IfLike(_, split) => Vector.single(split) + case SynthIf(split) => Vector.single(split) + case SynthSel(pre, nme) => Vector.double(pre, nme) + case Sel(pre, nme) => Vector.double(pre, nme) + case SelProj(prefix, cls, proj) => Vector.triple(prefix, cls, proj) case _ => subTerms // TODO more precise (include located things that aren't terms) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala index 403b459d43..bfc4f8686f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Pattern.scala @@ -49,7 +49,7 @@ sealed abstract class Pattern[+K <: Kind.Complete] extends AutoLocated: // TODO: Associate with locations. protected def children: Vector[Located] = this match - case Literal(lit) => Vector(lit) + case Literal(lit) => Vector.single(lit) case ClassLike(sym, arguments) => arguments.fold(Vector.empty): _.map((id, p) => p).toVector case Record(entries) => entries.values.toVector @@ -57,9 +57,9 @@ sealed abstract class Pattern[+K <: Kind.Complete] extends AutoLocated: case (_, middle, trailing) => middle +: trailing.toVector case And(patterns) => patterns.toVector case Or(patterns) => patterns.toVector - case Not(pattern) => Vector(pattern) - case Rename(pattern, name) => Vector(pattern) - case Extract(pattern, _, term) => Vector(pattern, term) + case Not(pattern) => Vector.single(pattern) + case Rename(pattern, name) => Vector.single(pattern) + case Extract(pattern, _, term) => Vector.double(pattern, term) case Synonym(pattern) => pattern.symbol +: pattern.arguments.toVector lazy val symbols: Ls[VarSymbol] = this match diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala index e5c731af9f..9a4f36e332 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala @@ -121,49 +121,49 @@ enum Tree extends AutoLocated: def children: Vector[Located] = this match case _: Empty | _: Error | _: Ident | _: Literal | _: Under | _: Unt => Vector.empty - case Pun(_, e) => Vector(e) - case Bra(_, e) => Vector(e) + case Pun(_, e) => Vector.single(e) + case Bra(_, e) => Vector.single(e) case Block(stmts) => stmts.toVector case LetLike(kw, lhs, rhs, body) => lhs +: (rhs.toVector ++ body.toVector) case Hndl(lhs, rhs, defs, body) => body match case Some(value) => lhs +: rhs +: defs +: value +: Vector.empty case None => lhs +: rhs +: defs +: Vector.empty case TypeDef(k, head, rhs) => head +: rhs.toVector - case Modified(_, body) => Vector(body) - case Quoted(body) => Vector(body) - case Unquoted(body) => Vector(body) + case Modified(_, body) => Vector.single(body) + case Quoted(body) => Vector.single(body) + case Unquoted(body) => Vector.single(body) case Tup(fields) => fields.toVector - case App(lhs, rhs) => Vector(lhs, rhs) + case App(lhs, rhs) => Vector.double(lhs, rhs) case OpApp(lhs, op, rhss) => lhs +: op +: rhss.toVector - case Jux(lhs, rhs) => Vector(lhs, rhs) - case PrefixApp(kw, rhs) => Vector(kw, rhs) - case InfixApp(lhs, kw, rhs) => Vector(lhs, kw, rhs) + case Jux(lhs, rhs) => Vector.double(lhs, rhs) + case PrefixApp(kw, rhs) => Vector.double(kw, rhs) + case InfixApp(lhs, kw, rhs) => Vector.triple(lhs, kw, rhs) case TermDef(k, head, rhs) => head +: rhs.toVector case LexicalNew(body, rft) => body.toVector ++ rft.toVector case ProperNew(body, rft) => body.toVector ++ rft.toVector - case DynamicNew(body) => Vector(body) - case IfLike(_, split) => Vector(split) - case Case(_, bs) => Vector(bs) - case Region(name, body) => Vector(name, body) - case RegRef(reg, value) => Vector(reg, value) - case Effectful(eff, body) => Vector(eff, body) + case DynamicNew(body) => Vector.single(body) + case IfLike(_, split) => Vector.single(split) + case Case(_, bs) => Vector.single(bs) + case Region(name, body) => Vector.double(name, body) + case RegRef(reg, value) => Vector.double(reg, value) + case Effectful(eff, body) => Vector.double(eff, body) case Outer(name) => name.toVector case TyTup(tys) => tys.toVector - case Sel(prefix, name) => Vector(prefix) - case SynthSel(prefix, name) => Vector(prefix) - case DynAccess(prefix, fld) => Vector(prefix, fld) - case Open(bod) => Vector(bod) - case OpenIn(opened, body) => Vector(opened, body) - case Def(lhs, rhs) => Vector(lhs, rhs) - case Spread(kw, body) => Vector(kw) ++ body.toVector - case Annotated(annotation, target) => Vector(annotation, target) - case Constructor(decl) => Vector(decl) - case MemberProj(cls, name) => Vector(cls) + case Sel(prefix, name) => Vector.single(prefix) + case SynthSel(prefix, name) => Vector.single(prefix) + case DynAccess(prefix, fld) => Vector.double(prefix, fld) + case Open(bod) => Vector.single(bod) + case OpenIn(opened, body) => Vector.double(opened, body) + case Def(lhs, rhs) => Vector.double(lhs, rhs) + case Spread(kw, body) => Vector.single(kw) ++ body.toVector + case Annotated(annotation, target) => Vector.double(annotation, target) + case Constructor(decl) => Vector.single(decl) + case MemberProj(cls, name) => Vector.single(cls) case Keywrd(kw) => Vector.empty case Dummy => Vector.empty case OpSplit(lhs, ops_rhss) => lhs +: ops_rhss.toVector case SplitPoint() => Vector.empty - case Trm(trm) => Vector(trm) + case Trm(trm) => Vector.single(trm) def describe: Str = this match case Empty() => "empty"