feat: add option flag, greedy compilation of multiple files, and refactor to use paths instead of files

This commit is contained in:
Jonny
2025-03-02 03:12:53 +00:00
parent 01b38b1445
commit 85a82aabb4
4 changed files with 73 additions and 49 deletions

View File

@@ -14,7 +14,6 @@
//> using test.dep org.scalatest::scalatest::3.2.19 //> using test.dep org.scalatest::scalatest::3.2.19
//> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0 //> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0
// sensible defaults for warnings and compiler checks // sensible defaults for warnings and compiler checks
//> using options -deprecation -unchecked -feature //> using options -deprecation -unchecked -feature
//> using options -Wimplausible-patterns -Wunused:all //> using options -Wimplausible-patterns -Wunused:all

View File

@@ -1,10 +1,11 @@
package wacc package wacc
import scala.collection.mutable import scala.collection.mutable
import cats.data.Chain import cats.data.{Chain, NonEmptyList}
import parsley.{Failure, Success} 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.IO
import cats.effect.ExitCode import cats.effect.ExitCode
@@ -18,24 +19,30 @@ import org.typelevel.log4cats.Logger
import assemblyIR as asm import assemblyIR as asm
given Argument[File] = Argument.from("file") { str =>
val file = File(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)
}
val cliCommand: Command[File] = // TODO: IO correctness, --greedy, parallelisable, and probably splitting this file up
Command("wacc-compiler", "Compile WACC programs") {
Opts.argument[File]("file")
given Argument[Path] = Argument.from("path") { str =>
val path = Path.of(str)
(
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
} }
given logger: Logger[IO] = Slf4jLogger.getLogger[IO] 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( def frontend(
contents: String contents: String
): IO[Either[Int, microWacc.Program]] = { ): IO[Either[Int, microWacc.Program]] = {
@@ -51,55 +58,73 @@ def frontend(
val typedProg = typeChecker.check(prog) 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 { else {
// TODO: multiple traversal of error content, should be a foldleft or co
given errorContent: String = contents given errorContent: String = contents
val exitCode = errors.result.view.map { val exitCode = errResult.collectFirst {
case _: Error.InternalError => 201 case _: Error.InternalError => 201
case _ => 200 }.getOrElse(200)
}.max
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] = def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] =
asmGenerator.generateAsm(typedProg) 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 { for {
contents <- IO(os.read(os.Path(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?
_ <- logger.info(s"Compiling file: $filename") _ <- logAction(s"Compiling file: $filename")
result <- frontend(contents) result <- frontend(contents)
exitCode <- result.fold( exitCode <- result.fold(
code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code),
typedProg => typedProg =>
val outputFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s")) val outDir = outputDir.getOrElse(Paths.get(filename).getParent)
writer.writeTo(backend(typedProg), outputFile) *> logger IO.delay(Files.createDirectories(outDir)) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking?
.info(s"Compilation succeeded: $filename") val outputFile = outDir.resolve(filename.stripSuffix(".wacc") + ".s")
.as(0) 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 } 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 object Main
extends CommandIOApp( extends CommandIOApp(
name = "wacc-compiler", name = "wacc",
header = "the ultimate wacc compiler", header = "The ultimate WACC compiler",
version = "1.0" version = "1.0"
) { ) {
def main: Opts[IO[ExitCode]] = def main: Opts[IO[ExitCode]] =
Opts.arguments[File]("files").map { files => compileCommand
files
.parTraverse_ { file =>
compile(
file.getAbsolutePath,
outFile = Some(File(".", file.getName.stripSuffix(".wacc") + ".s"))
)
}
.as(ExitCode.Success)
}
} }

View File

@@ -2,26 +2,26 @@ package wacc
import cats.effect.Resource import cats.effect.Resource
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.io.File
import java.io.BufferedWriter import java.io.BufferedWriter
import java.io.FileWriter import java.io.FileWriter
import cats.data.Chain import cats.data.Chain
import cats.effect.IO import cats.effect.IO
import org.typelevel.log4cats.Logger import org.typelevel.log4cats.Logger
import java.nio.file.Path
object writer { object writer {
import assemblyIR._ 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 Resource
.fromAutoCloseable { .fromAutoCloseable {
IO(BufferedWriter(FileWriter(outputFile, StandardCharsets.UTF_8))) IO(BufferedWriter(FileWriter(outputPath.toFile, StandardCharsets.UTF_8)))
} }
.use { writer => .use { writer =>
IO { IO {
asmList.iterator.foreach(line => writer.write(line.toString + "\n")) asmList.iterator.foreach(line => writer.write(line.toString + "\n"))
writer.flush() // TODO: NECESSARY OR NOT? writer.flush() // TODO: NECESSARY OR NOT?
} *> logger.info(s"Wrote assembly to ${outputFile.getAbsolutePath}") } *> logger.info(s"Success: ${outputPath.toAbsolutePath}")
} }
} }

View File

@@ -32,7 +32,7 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd
s"$filename" - { s"$filename" - {
"should be compiled with correct result" in { "should be compiled with correct result" in {
compileWacc(filename).map { result => compileWacc(filename, outputDir = None, log = false).map { result =>
expectedResult should contain(result) expectedResult should contain(result)
} }
} }