feat: add option flag, greedy compilation of multiple files, and refactor to use paths instead of files
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
// 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"),
|
Either.cond(Files.exists(path), path, s"File '${path.toAbsolutePath}' does not exist"),
|
||||||
Option
|
Either.cond(Files.isRegularFile(path), path, s"File '${path.toAbsolutePath}' must be a regular file"),
|
||||||
.when(file.isFile())(file)
|
Either.cond(path.toString.endsWith(".wacc"), path, "File must have .wacc extension")
|
||||||
.toValidNel(s"File '${file.getAbsolutePath}' must be a regular file"),
|
).mapN((_, _, _) => path).toValidatedNel
|
||||||
Option.when(file.getName.endsWith(".wacc"))(file).toValidNel("File must have .wacc extension")
|
|
||||||
).mapN((_, _, _) => file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val cliCommand: Command[File] =
|
|
||||||
Command("wacc-compiler", "Compile WACC programs") {
|
|
||||||
Opts.argument[File]("file")
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user