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 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

View File

@@ -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
}

View File

@@ -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}")
}
}

View File

@@ -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)
}
}