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/build.sbt b/build.sbt index 179c82787c..20f37f82f1 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,5 @@ import Wart._ +import org.scalajs.linker.interface.OutputPatterns enablePlugins(ScalaJSPlugin) @@ -42,7 +43,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" % scalaTestVersion, @@ -57,6 +59,13 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) ) .jvmSettings( ) + .jsSettings( + scalaJSLinkerConfig ~= { + _.withModuleKind(ModuleKind.ESModule) + .withOutputPatterns(OutputPatterns.fromJSFile("MLscript.mjs")) + }, + libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.2.0", + ) .dependsOn(core) lazy val hkmc2JVM = hkmc2.jvm 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/js/src/main/scala/hkmc2/Compiler.scala b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala new file mode 100644 index 0000000000..f7f5691c59 --- /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 + + 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.toArray.sortBy(_._2._1).map: + case (path, (_, diagnostics)) => js.Dynamic.literal( + path = path, + diagnostics = diagnostics.iterator.map: d => + js.Dynamic.literal( + kind = d.kind.toString().toLowerCase(), + source = d.source.toString().toLowerCase(), + mainMessage = d.theMsg, + allMessages = d.allMsgs.iterator.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/io/InMemoryFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala new file mode 100644 index 0000000000..48b05723e0 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala @@ -0,0 +1,41 @@ +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 + * 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 = read(path.toString) + + def write(path: Path, content: String): Unit = + write(path.toString, content) + + def exists(path: Path): Bool = files.contains(path.toString) + + @JSExport("write") + def write(path: Str, content: Str): Unit = + 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 + +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) 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..1d1d732392 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/PlatformPath.scala @@ -0,0 +1,10 @@ +package hkmc2.io + +/** + * Platform-specific factory for creating Path instances + */ +private[io] object PathFactory: + 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..422c218816 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala @@ -0,0 +1,113 @@ +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/node/PlatformFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala new file mode 100644 index 0000000000..2c24619540 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/node/PlatformFileSystem.scala @@ -0,0 +1,33 @@ +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 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) + +private[io] object PlatformFileSystem: + def default: FileSystem = new NodeFileSystem 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..9ea60c101e --- /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/test/scala/hkmc2/io/VirtualPathTests.scala b/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala new file mode 100644 index 0000000000..d9e5b5355e --- /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/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..feaf7f76a5 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 @@ -44,26 +45,29 @@ class CompileTestRunner test(relativeName): - println(s"Compiling: $relativeName") - - val preludePath = mainTestDir/"mlscript"/"decls"/"Prelude.mls" + 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 + // Synchronize diagnostic output to avoid interleaving since the compiler tests run in parallel. + val wrap: (=> Unit) => Unit = body => CompileTestRunner.synchronized(body) + val report = ReportFormatter(System.out.println, colorize = true, wrap = Some(wrap)) val compiler = MLsCompiler( - preludePath, - mkOutput => - // * Synchronize diagnostic output to avoid interleaving since the compiler tests run in parallel - CompileTestRunner.synchronized: - mkOutput(System.out.println) + 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 = report.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/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..89bcc51312 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -3,18 +3,20 @@ package hkmc2 import scala.collection.mutable import mlscript.utils.*, shorthands.* +import hkmc2.io 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} -class ParserSetup(file: os.Path, dbgParsing: Bool)(using Elaborator.State, Raise): +class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State, raise: Raise, fs: io.FileSystem): - val block = os.read(file) + val block = fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) @@ -33,20 +35,25 @@ class ParserSetup(file: os.Path, dbgParsing: Bool)(using Elaborator.State, Raise val result = parser.parseAll(parser.block(allowNewlines = true)) val resultBlk = new syntax.Tree.Block(result) - +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 -// * 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" - +/** + * 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 @@ -58,14 +65,11 @@ class MLsCompiler(preludeFile: os.Path, mkOutput: ((Str => Unit) => Unit) => Uni var dbgParsing = false - def compileModule(file: os.Path): Unit = + def compileModule(file: io.Path): Unit = - val wd = file / os.up + val wd = file.up - given raise: Raise = d => - mkOutput: - _(fansi.Color.LightRed(s"/!!!\\ Error in ${file.relativeTo(wd/os.up)} /!!!\\").toString) - report(0, d :: Nil, showRelativeLineNums = false) + given Raise = mkRaise(file) given Elaborator.State = new Elaborator.State @@ -84,10 +88,18 @@ class MLsCompiler(preludeFile: os.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, + 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. + (if hasQuote then + Import(State.termSymbol, termFile.toString, termFile) :: blk0.stats + else + blk0.stats), blk0.res ) val low = ltl.givenIn: @@ -107,8 +119,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/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index d3aa3a5908..879737680c 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.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.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/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index ca6242e94c..45bb698e80 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -828,9 +828,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 7b9d9c1e32..8694a4249c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala @@ -596,14 +596,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 => @@ -612,7 +612,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 6463d17b2b..53be795eac 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 @@ -832,7 +832,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..2a6eaba084 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala @@ -0,0 +1,29 @@ +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 + + 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/Path.scala b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala new file mode 100644 index 0000000000..d3b90f8cfa --- /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 2980288e9b..74c16ecc37 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -315,8 +315,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..f52eff9a23 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.* @@ -15,14 +16,14 @@ 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 @@ -42,7 +43,7 @@ class Importer: case "mls" => - val block = os.read(file) + val block = fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) @@ -61,14 +62,14 @@ class Importer: 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/Pattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala index cd1c29ffe3..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) @@ -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.double(left, right) + case Negation(pattern) => Vector.single(pattern) + case Wildcard() => Vector.empty + 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.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 - 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..9b8be228a5 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.double(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.single(default) + case S(kw) => Vector.double(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.single(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.single(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.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 695e40453b..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: List[Located] = scrutinee :: pattern :: continuation :: Nil + override def children: Vector[Located] = Vector.triple(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.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: 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.single(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 3c18cae966..875c3a2b8b 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.single(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.single(trm) + case _: Modifier | Untyped | TailRec | TailCall => Vector.empty def mkClone(using State): Annot = this match case Untyped => Untyped @@ -454,79 +454,79 @@ 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.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.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.single(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.single(body) + case Blk(stats, res) => stats.flatMap(_.subTerms).toVector :+ res + case Rcd(mut, stats) => stats.flatMap(_.subTerms).toVector + 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.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.double(lhs, rhs) + case LetDecl(sym, annotations) => annotations.flatMap(_.subTerms).toVector + 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.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.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.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): Ls[Located] = t match + private def treeOrSubterms(t: Tree): Vector[Located] = t match case Tree.DummyApp | Tree.DummyTup => subTerms - case _ => t :: Nil + case _ => Vector.single(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.single(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.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) @@ -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: @@ -986,12 +986,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 @@ -1018,7 +1018,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..bfc4f8686f 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.single(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.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 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..9a4f36e332 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.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 :: 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.single(body) + case Quoted(body) => Vector.single(body) + case Unquoted(body) => Vector.single(body) + case Tup(fields) => fields.toVector + case App(lhs, rhs) => Vector.double(lhs, rhs) + case OpApp(lhs, op, rhss) => lhs +: op +: rhss.toVector + 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.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.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.single(trm) def describe: Str = this match case Empty() => "empty" diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala index 78cc1d2cb5..6a747180e4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala @@ -5,12 +5,40 @@ import collection.mutable import mlscript.utils.*, shorthands.* -class ReportFormatter(mkOutput: ((Str => Unit) => Unit) => Unit): +/** + * 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, + 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 - def apply(blockLineNum: Int, diags: Ls[Diagnostic], showRelativeLineNums: Bool): Unit = mkOutput: output => + /** 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 = diags.foreach { diag => val sctx = Message.mkCtx(diag.allMsgs.iterator.map(_._1), "?") val onlyOneLine = diag.allMsgs.size =:= 1 && diag.allMsgs.head._2.isEmpty @@ -38,10 +66,10 @@ class ReportFormatter(mkOutput: ((Str => Unit) => Unit) => Unit): 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) @@ -60,7 +88,7 @@ class ReportFormatter(mkOutput: ((Str => Unit) => Unit) => Unit): 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) @@ -68,23 +96,23 @@ class ReportFormatter(mkOutput: ((Str => Unit) => Unit) => Unit): 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 } // } () diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs index 5a9d766890..f7c5775150 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 d5fcb5145e..014b000f74 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/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..32dc0dd121 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,13 +136,12 @@ 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) val output = Outputter(out) - val report = ReportFormatter: outputConsumer => - outputConsumer(output(_)) + val report = ReportFormatter(output(_), colorize = false) val failures = mutable.Buffer.empty[Int] val unmergedChanges = mutable.Buffer.empty[Int] @@ -351,7 +351,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 7c71cd21c9..db1f821638 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 0fbbce6bd1..89f1298020 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 @@ -107,7 +107,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 @@ -173,14 +173,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) @@ -255,7 +255,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..d07e442544 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 +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 // 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,17 @@ class Watcher(dirs: Ls[File]): if isModuleFile then given Config = Config.default - MLsCompiler(preludePath, outputConsumer => outputConsumer(System.out.println)).compileModule(path) + given FileSystem = FileSystem.default + 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 = true).mkRaise + ).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")