diff --git a/project.scala b/project.scala index e4f1c0f..d8b5880 100644 --- a/project.scala +++ b/project.scala @@ -14,7 +14,6 @@ //> using test.dep org.scalatest::scalatest::3.2.19 //> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0 - // sensible defaults for warnings and compiler checks //> using options -deprecation -unchecked -feature //> using options -Wimplausible-patterns -Wunused:all diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index ad43479..f4c290b 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -1,10 +1,11 @@ package wacc import scala.collection.mutable -import cats.data.Chain +import cats.data.{Chain, NonEmptyList} import parsley.{Failure, Success} -import java.io.File -import cats.implicits.* + +import java.nio.file.{Files, Path, Paths} +import cats.syntax.all._ import cats.effect.IO import cats.effect.ExitCode @@ -18,24 +19,30 @@ import org.typelevel.log4cats.Logger import assemblyIR as asm -given Argument[File] = Argument.from("file") { str => - val file = File(str) + +// TODO: IO correctness, --greedy, parallelisable, and probably splitting this file up + + +given Argument[Path] = Argument.from("path") { str => + val path = Path.of(str) ( - Option.when(file.exists())(file).toValidNel(s"File '${file.getAbsolutePath}' does not exist"), - Option - .when(file.isFile())(file) - .toValidNel(s"File '${file.getAbsolutePath}' must be a regular file"), - Option.when(file.getName.endsWith(".wacc"))(file).toValidNel("File must have .wacc extension") - ).mapN((_, _, _) => file) + Either.cond(Files.exists(path), path, s"File '${path.toAbsolutePath}' does not exist"), + Either.cond(Files.isRegularFile(path), path, s"File '${path.toAbsolutePath}' must be a regular file"), + Either.cond(path.toString.endsWith(".wacc"), path, "File must have .wacc extension") + ).mapN((_, _, _) => path).toValidatedNel } -val cliCommand: Command[File] = - Command("wacc-compiler", "Compile WACC programs") { - Opts.argument[File]("file") - } - given logger: Logger[IO] = Slf4jLogger.getLogger[IO] +val logOpt: Opts[Boolean] = + Opts.flag("log", "Enable logging for additional compilation details", short = "l").orFalse + +val outputOpt: Opts[Option[Path]] = + Opts.option[Path]("output", "Specify path for output assembly file(s)").orNone + +val filesOpt: Opts[NonEmptyList[Path]] = Opts.arguments[Path]("files") + + def frontend( contents: String ): IO[Either[Int, microWacc.Program]] = { @@ -51,55 +58,73 @@ def frontend( val typedProg = typeChecker.check(prog) - if (errors.result.isEmpty) IO.pure(Right(typedProg)) + val errResult = errors.result + + if (errResult.isEmpty) IO.pure(Right(typedProg)) else { + // TODO: multiple traversal of error content, should be a foldleft or co given errorContent: String = contents - val exitCode = errors.result.view.map { + val exitCode = errResult.collectFirst { case _: Error.InternalError => 201 - case _ => 200 - }.max + }.getOrElse(200) - val formattedErrors = errors.result.map(formatError).mkString("\n") + val formattedErrors = errResult.map(formatError).mkString("\n") - logger.error(s"Semantic errors:\n$formattedErrors") *> IO.pure(Left(exitCode)) + logger.error(s"Semantic errors:\n$formattedErrors").as(Left(exitCode)) } } } -val s = "enter an integer to echo" def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = asmGenerator.generateAsm(typedProg) -def compile(filename: String, outFile: Option[File] = None): IO[Int] = +// TODO: filename being String seems unnatural due to Path refactor +// TODO: this function is doing too much should refactor +def compile(filename: String, outputDir: Option[Path], log: Boolean): IO[Int] = + val logAction: String => IO[Unit] = + if (log) logger.info(_) + else (_ => IO.unit) for { - contents <- IO(os.read(os.Path(filename))) - _ <- logger.info(s"Compiling file: $filename") + contents <- IO.delay(os.read(os.Path(filename))) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? + _ <- logAction(s"Compiling file: $filename") result <- frontend(contents) exitCode <- result.fold( code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), typedProg => - val outputFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s")) - writer.writeTo(backend(typedProg), outputFile) *> logger - .info(s"Compilation succeeded: $filename") - .as(0) + val outDir = outputDir.getOrElse(Paths.get(filename).getParent) + IO.delay(Files.createDirectories(outDir)) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? + val outputFile = outDir.resolve(filename.stripSuffix(".wacc") + ".s") + writer.writeTo(backend(typedProg), outputFile) *> // TODO: I dont think we need IO here if we look at the implementation of writer + logAction(s"Compilation succeeded: $filename").as(0) ) } yield exitCode +// TODO: this is sequential, thus should be what occurs when --greedy is passed in +val compileCommand: Opts[IO[ExitCode]] = + (filesOpt, logOpt, outputOpt).mapN{ + (files, log, outDir) => + files + .traverse{ file => + compile( + file.toAbsolutePath.toString, + outDir, + log) + }.map { + exitCodes => + if (exitCodes.exists(_ != 0)) ExitCode.Error // TODO- it should be the first one to exit when parallelised :) + else ExitCode.Success + } + + } + +// TODO: add parallelisable option object Main extends CommandIOApp( - name = "wacc-compiler", - header = "the ultimate wacc compiler", + name = "wacc", + header = "The ultimate WACC compiler", version = "1.0" ) { def main: Opts[IO[ExitCode]] = - Opts.arguments[File]("files").map { files => - files - .parTraverse_ { file => - compile( - file.getAbsolutePath, - outFile = Some(File(".", file.getName.stripSuffix(".wacc") + ".s")) - ) - } - .as(ExitCode.Success) - } -} + compileCommand + +} \ No newline at end of file diff --git a/src/main/wacc/backend/writer.scala b/src/main/wacc/backend/writer.scala index b87ec01..8789835 100644 --- a/src/main/wacc/backend/writer.scala +++ b/src/main/wacc/backend/writer.scala @@ -2,26 +2,26 @@ package wacc import cats.effect.Resource import java.nio.charset.StandardCharsets -import java.io.File import java.io.BufferedWriter import java.io.FileWriter import cats.data.Chain import cats.effect.IO import org.typelevel.log4cats.Logger +import java.nio.file.Path object writer { import assemblyIR._ - def writeTo(asmList: Chain[AsmLine], outputFile: File)(using logger: Logger[IO]): IO[Unit] = + def writeTo(asmList: Chain[AsmLine], outputPath: Path)(using logger: Logger[IO]): IO[Unit] = Resource .fromAutoCloseable { - IO(BufferedWriter(FileWriter(outputFile, StandardCharsets.UTF_8))) + IO(BufferedWriter(FileWriter(outputPath.toFile, StandardCharsets.UTF_8))) } .use { writer => IO { asmList.iterator.foreach(line => writer.write(line.toString + "\n")) writer.flush() // TODO: NECESSARY OR NOT? - } *> logger.info(s"Wrote assembly to ${outputFile.getAbsolutePath}") + } *> logger.info(s"Success: ${outputPath.toAbsolutePath}") } } diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index 76e84ed..ca1fe9d 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -32,7 +32,7 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd s"$filename" - { "should be compiled with correct result" in { - compileWacc(filename).map { result => + compileWacc(filename, outputDir = None, log = false).map { result => expectedResult should contain(result) } }