diff --git a/core/shared/main/scala/utils/package.scala b/core/shared/main/scala/utils/package.scala index 2cb16190bf..47f1ced2a1 100644 --- a/core/shared/main/scala/utils/package.scala +++ b/core/shared/main/scala/utils/package.scala @@ -185,6 +185,15 @@ package object utils { case h :: t => f(h) :: t case Nil => Nil } + def takeWhileAndRest(p: A => Boolean): (Ls[A], Ls[A]) = { + val b = new mutable.ListBuffer[A] + var ys = ls + while (!ys.isEmpty && p(ys.head)) { + b += ys.head + ys = ys.tail + } + (b.toList, ys) + } } implicit final class OptionHelpers[A](opt: Opt[A]) { diff --git a/hkmc2/shared/src/test/mlscript/commands/StatefulComments.mls b/hkmc2/shared/src/test/mlscript/commands/StatefulComments.mls new file mode 100644 index 0000000000..ff88a08a66 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/commands/StatefulComments.mls @@ -0,0 +1,38 @@ +:js + +:clock 10000 +let i = 0 +while i < 10000 do + set i += 1 +//+ runtime clock time (10000): 0.761641 ms +//│ i = 10000 + +:clock 1000000 +let i = 0 +while i < 1000000 do + set i += 1 +//+ runtime clock time (1000000): 6.105116 ms +//│ i = 1000000 + +:freeze date +Date.now() +//+ freeze (date): = 1754638286509 + +fun choose(choices) = + val index = Math.floor(Math.random() * choices.length) + choices.[index] + +:freeze choose +let c = choose(["a", "b", "c"]) +//+ freeze (choose): c = "b" + +:freeze choose +if c == "a" then + "You chose a!" +else if c == "b" then + "You chose b!" +else + "You chose c!" +//+ freeze (choose): = "You chose b!" + + diff --git a/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala b/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala index aaeded43bb..92d0d34f98 100644 --- a/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala +++ b/hkmc2Benchmarks/src/test/scala/hkmc2/BenchDiffMaker.scala @@ -7,5 +7,5 @@ import hkmc2.syntax.Keyword class BenchDiffMaker(val rootPath: Str, val file: os.Path, val preludeFile: os.Path, val predefFile: os.Path, val relativeName: Str) extends LlirDiffMaker: - override def processTerm(blk: semantics.Term.Blk, inImport: Bool)(using Config, Raise): Unit = - super.processTerm(blk, inImport) + override def processTerm(blk: semantics.Term.Blk, inImport: Bool, statefulComments: Ls[String])(using Config, Raise): Unit = + super.processTerm(blk, inImport, statefulComments) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala index 71b8248465..9850a4d777 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala @@ -32,8 +32,8 @@ abstract class BbmlDiffMaker extends JSBackendDiffMaker: var bbmlTyper: Opt[BBTyper] = None - override def processTerm(trm: semantics.Term.Blk, inImport: Bool)(using Config, Raise): Unit = - super.processTerm(trm, inImport) + override def processTerm(trm: semantics.Term.Blk, inImport: Bool, statefulComments: Ls[String])(using Config, Raise): Unit = + super.processTerm(trm, inImport, statefulComments) if bbmlOpt.isSet then given Scope = Scope.empty if bbmlTyper.isEmpty then diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala index 60abdaa9db..110c4b850a 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala @@ -2,6 +2,7 @@ package hkmc2 import scala.collection.mutable import mlscript.utils.*, shorthands.* +import scala.collection.mutable.ListBuffer @@ -17,6 +18,8 @@ class Outputter(val out: java.io.PrintWriter): val exitMarker = "=" * 100 val blockSeparator = "—" * 80 + + val statefulMarker = "//+ " val fullBlockSeparator = outputMarker + blockSeparator @@ -24,6 +27,8 @@ class Outputter(val out: java.io.PrintWriter): // out.println(outputMarker + str) str.splitSane('\n').foreach(l => out.println(outputMarker + l)) + def stateful(str: String) = + str.splitSane('\n').foreach(l => out.println(statefulMarker + l)) abstract class DiffMaker: @@ -31,7 +36,7 @@ abstract class DiffMaker: val file: os.Path val relativeName: Str - def processOrigin(origin: Origin)(using Raise): Unit + def processOrigin(origin: Origin, statefulComments: List[String])(using Raise): Unit @@ -162,7 +167,7 @@ abstract class DiffMaker: ).map("\n" + "\tat: " + _).mkString) - def processBlock(origin: Origin): Unit = + def processBlock(origin: Origin, statefulComments: List[String]): Unit = val globalStartLineNum = origin.startLineNum val blockLineNum = origin.startLineNum // * ^ In previous DiffTest versions, these two could be different due to relative line numbers @@ -212,7 +217,7 @@ abstract class DiffMaker: throw d report(blockLineNum, d :: Nil, showRelativeLineNums.isSet) - processOrigin(origin)(using raise) + processOrigin(origin, statefulComments)(using raise) // Note: when `todo` is set, we allow the lack of errors. // Use `todo` when the errors are expected but not yet implemented. @@ -270,8 +275,8 @@ abstract class DiffMaker: output("/!\\ Unrecognized command: " + cmd) rec(ls) - case line :: ls if line.startsWith(output.outputMarker) //|| line.startsWith(oldOutputMarker) - => rec(ls) + case line :: ls if line.startsWith(output.outputMarker) || line.startsWith(output.statefulMarker) + => rec(ls) //|| line.startsWith(oldOutputMarker) case line :: ls if line.startsWith("//") => out.println(line) rec(ls) @@ -301,21 +306,32 @@ abstract class DiffMaker: val blockLineNum = allLines.size - lines.size + 1 - val block = (l :: ls.takeWhile(l => (l.nonEmpty || consumeEmptyLines.isSet) && !( + val (blockU, rest) = (l :: ls).takeWhileAndRest((l => (l.nonEmpty || consumeEmptyLines.isSet) && !( l.startsWith(output.outputMarker) || l.startsWith(output.diffBegMarker) + || l.startsWith(output.statefulMarker) // || l.startsWith(oldOutputMarker) - ))).toIndexedSeq + ))) + val block = blockU.toIndexedSeq block.foreach(out.println) val processedBlock = block val processedBlockStr = processedBlock.mkString val fph = new FastParseHelpers(block) val origin = Origin(file, blockLineNum, fph) - + val statefulComments = rest.takeWhile(l => l.nonEmpty && ( + l.startsWith(output.statefulMarker) + || l.startsWith(output.outputMarker) + || l.startsWith(output.diffBegMarker) + || l.startsWith(output.diffMidMarker) + || l.startsWith(output.diff3MidMarker) + || l.startsWith(output.diffEndMarker) + )).filter(l => l.startsWith(output.statefulMarker)) + .map(l => l.stripPrefix(output.statefulMarker).trim) + try - processBlock(origin) + processBlock(origin, statefulComments) catch case oh_noes: ThreadDeath => throw oh_noes diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 085f7411ff..7780ca4f0b 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -24,6 +24,12 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val showJS = NullaryCommand("sjs") val showRepl = NullaryCommand("showRepl") val traceJS = NullaryCommand("traceJS") + val runtimeClock = Command("clock")(_.trim) + val runtimeClockPrefix = "runtime clock time" + + val statefulOutput = Command("freeze")(_.trim) + val statatefulOutputPrefix = "freeze" + val expect = Command("expect"): ln => ln.trim @@ -63,8 +69,8 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: override def run(): Unit = try super.run() finally if hostCreated then host.terminate() - override def processTerm(blk: semantics.Term.Blk, inImport: Bool)(using Config, Raise): Unit = - super.processTerm(blk, inImport) + override def processTerm(blk: semantics.Term.Blk, inImport: Bool, statefulComments: Ls[String])(using Config, Raise): Unit = + super.processTerm(blk, inImport, statefulComments) val outerRaise: Raise = summon val reportedMessages = mutable.Set.empty[Str] @@ -162,11 +168,28 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: // * Sometimes the JS block won't execute due to a syntax or runtime error so we always set this first host.execute(s"$resNme = undefined") + + val timingId = runtimeClock.get.flatMap(s => if s == "" then N else S(s)) + val startTime = timingId.map(_ => System.nanoTime()) mkQuery(preStr, jsStr): stdout => stdout.splitSane('\n').init // should always ends with "undefined" (TODO: check) .foreach: line => output(s"> ${line}") + + startTime.foreach: start => + val endTime = System.nanoTime() + val elapsedMs = (endTime - start) / 1_000_000.0 + statefulComments.find(_.startsWith(runtimeClockPrefix)) match + case S(time) => + val cachedId = time.stripPrefix(s"${runtimeClockPrefix} (").split(')').headOption.getOrElse("") + if cachedId == timingId.get then + output.stateful(time) + else + output.stateful(s"${runtimeClockPrefix} (${timingId.get}): ${elapsedMs} ms") + case N => + output.stateful(s"${runtimeClockPrefix} (${timingId.get}): ${elapsedMs} ms") + if traceJS.isSet then host.execute(s"$runtimeNme.TraceLogger.enabled = false") @@ -205,6 +228,19 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: case "undefined" if anon => case "()" if anon => case _ => - output(s"${if anon then "" else s"$nme "}= ${result.indentNewLines("| ")}") - + val outputStr = s"${if anon then "" else s"$nme "}= ${result.indentNewLines("| ")}" + val currId = statefulOutput.get.getOrElse("") + // TODO: avoid execution if cache not invalidated + if statefulOutput.isSet then + statefulComments.find(_.startsWith(statatefulOutputPrefix)) match + case S(prev) => + val cachedId = prev.stripPrefix(s"${statatefulOutputPrefix} (").split(')').headOption.getOrElse("") + if cachedId == currId then + output.stateful(prev) + else + output.stateful(s"${statatefulOutputPrefix} (${currId}): ${outputStr}") + case N => + output.stateful(s"${statatefulOutputPrefix} (${currId}): ${outputStr}") + else + output(outputStr) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/LlirDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/LlirDiffMaker.scala index cba44a064f..e732ea3df7 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/LlirDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/LlirDiffMaker.scala @@ -59,8 +59,8 @@ abstract class LlirDiffMaker extends BbmlDiffMaker: entry = wholeProg.last.entry ) - override def processTerm(trm: Blk, inImport: Bool)(using Config, Raise): Unit = - super.processTerm(trm, inImport) + override def processTerm(trm: Blk, inImport: Bool, statefulComments: Ls[String])(using Config, Raise): Unit = + super.processTerm(trm, inImport, statefulComments) if llir.isSet then val low = ltl.givenIn: codegen.Lowering() diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 21e4866c50..2717edda39 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -145,11 +145,11 @@ abstract class MLsDiffMaker extends DiffMaker: processTrees( Modified(`import`, N, StrLit(predefFile.toString)) :: Open(Ident("Predef")) - :: Nil) + :: Nil, Nil) if importQQ.isSet then given Config = mkConfig processTrees( - Modified(`import`, N, StrLit(termFile.toString)) :: Nil) + Modified(`import`, N, StrLit(termFile.toString)) :: Nil, Nil) super.init() @@ -185,7 +185,7 @@ abstract class MLsDiffMaker extends DiffMaker: if verbose then output(s"Imported ${resBlk.definedSymbols.size} member(s)") curCtx = ctxWithImports - processTerm(e, inImport = true) + processTerm(e, inImport = true, Ls.empty) catch case err: Throwable => uncaught(err) @@ -195,7 +195,7 @@ abstract class MLsDiffMaker extends DiffMaker: override def emitDbg(str: String): Unit = output(str) - def processOrigin(origin: Origin)(using Raise): Unit = + def processOrigin(origin: Origin, statefulComments: List[String])(using Raise): Unit = val oldCtx = curCtx given Config = mkConfig @@ -223,7 +223,7 @@ abstract class MLsDiffMaker extends DiffMaker: // output(s"AST: $res") if parseOnly.isUnset then - processTrees(res)(using summon, raise) + processTrees(res, statefulComments)(using summon, raise) if showContext.isSet then output("Env:") @@ -234,7 +234,7 @@ abstract class MLsDiffMaker extends DiffMaker: private var blockNum = 0 - def processTrees(trees: Ls[syntax.Tree])(using Config, Raise): Unit = + def processTrees(trees: Ls[syntax.Tree], statefulComments: Ls[String])(using Config, Raise): Unit = val elab = Elaborator(etl, file / os.up, prelude) // val blockSymbol = // semantics.TopLevelSymbol("block#"+blockNum) @@ -251,11 +251,11 @@ abstract class MLsDiffMaker extends DiffMaker: output(s"Elaborated tree:") output(e.showAsTree(using post)) - processTerm(e, inImport = false) + processTerm(e, inImport = false, statefulComments) - def processTerm(trm: semantics.Term.Blk, inImport: Bool)(using Config, Raise): Unit = + def processTerm(trm: semantics.Term.Blk, inImport: Bool, statefulComments: Ls[String])(using Config, Raise): Unit = val resolver = Resolver(rtl) curICtx = resolver.traverseBlock(trm)(using curICtx)