feat: add option flag, greedy compilation of multiple files, and refactor to use paths instead of files
This commit is contained in:
@@ -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
|
||||
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user