Files
WACC_37/src/main/wacc/Main.scala

192 lines
6.2 KiB
Scala

package wacc
import scala.collection.mutable
import cats.data.{Chain, NonEmptyList}
import parsley.{Failure, Success}
import java.nio.file.{Files, Path}
import cats.syntax.all._
import cats.effect.IO
import cats.effect.ExitCode
import com.monovore.decline._
import com.monovore.decline.effect._
import com.monovore.decline.Argument
import org.typelevel.log4cats.slf4j.Slf4jLogger
import org.typelevel.log4cats.Logger
import assemblyIR as asm
import cats.data.ValidatedNel
/*
TODO:
1) IO correctness
2) Errors can be handled more gracefully - currently, parallelised compilation is not fail fast as far as I am aware
3) splitting the file up and nicer refactoring
4) logging could be removed
5) general cleanup and comments (things like replacing home/<user> with ~ , and names of parameters and args, descriptions etc)
*/
given logger: Logger[IO] = Slf4jLogger.getLogger[IO]
val logOpt: Opts[Boolean] =
Opts.flag("log", "Enable logging for additional compilation details", short = "l").orFalse
def validateFile(path: Path): ValidatedNel[String, Path] = {
(for {
// TODO: redundant 2nd parameter :(
_ <- Either.cond(Files.exists(path), (), s"File '${path}' does not exist")
_ <- Either.cond(Files.isRegularFile(path), (), s"File '${path}' must be a regular file")
_ <- Either.cond(path.toString.endsWith(".wacc"), (), "File must have .wacc extension")
} yield path).toValidatedNel
}
val filesOpt: Opts[NonEmptyList[Path]] =
Opts.arguments[Path]("files").mapValidated {
_.traverse(validateFile)
}
val outputOpt: Opts[Option[Path]] =
Opts
.option[Path]("output", metavar = "path", help = "Output directory for compiled files.")
.orNone
val greedyOpt: Opts[Boolean] =
Opts.flag("greedy", "Compile WACC files sequentially instead of parallelly", short = "g").orFalse
def frontend(
contents: String
): IO[Either[Int, microWacc.Program]] = {
IO(parser.parse(contents)).flatMap {
case Failure(msg) =>
logger.error(s"Syntax error: $msg").as(Left(100))
case Success(prog) =>
given errors: mutable.Builder[Error, List[Error]] = List.newBuilder
val (names, funcs) = renamer.rename(prog)
given ctx: typeChecker.TypeCheckerCtx = typeChecker.TypeCheckerCtx(names, funcs, errors)
val typedProg = typeChecker.check(prog)
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 = errResult
.collectFirst { case _: Error.InternalError =>
201
}
.getOrElse(200)
val formattedErrors = errResult.map(formatError).mkString("\n")
logger.error(s"Semantic errors:\n$formattedErrors").as(Left(exitCode))
}
}
}
def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] =
asmGenerator.generateAsm(typedProg)
def compile(filePath: Path, outputDir: Option[Path], log: Boolean): IO[Int] = {
val logAction: String => IO[Unit] =
if (log) logger.info(_)
else (_ => IO.unit)
def readSourceFile: IO[String] =
IO.blocking(os.read(os.Path(filePath)))
def ensureOutputDir(outDir: Path): IO[Path] =
IO.blocking {
Files.createDirectories(outDir)
outDir
}.handleErrorWith {
// TODO: I think this wont occur if a user runs with privileges but this must be checked
// TODO: this will return the ugly stack trace, one could refactor compileCommand to
case _: java.nio.file.AccessDeniedException =>
IO.raiseError(
new Exception(
s"Permission denied: Cannot create directory '${outDir.toAbsolutePath}'. Try choosing a different output path or run as root."
)
)
}
// TODO: path, file , the names are confusing (when Path is the type but we are working with files)
def writeOutputFile(typedProg: microWacc.Program, outputPath: Path): IO[Unit] =
writer.writeTo(backend(typedProg), outputPath) *>
logger.info(s"Success: ${outputPath.toAbsolutePath}")
def processProgram(contents: String, outDir: Path): IO[Int] =
frontend(contents).flatMap {
case Left(code) =>
logger.error(s"Compilation failed for $filePath\nExit code: $code").as(code)
case Right(typedProg) =>
val outputFile = outDir.resolve(filePath.getFileName.toString.stripSuffix(".wacc") + ".s")
writeOutputFile(typedProg, outputFile).as(0)
}
for {
contents <- readSourceFile
_ <- logAction(s"Compiling file: ${filePath.toAbsolutePath}")
outDir <- ensureOutputDir(outputDir.getOrElse(filePath.getParent))
exitCode <- processProgram(contents, outDir)
} yield exitCode
}
// TODO: Remove duplicate code between compileCommandSequential and compileCommandParallel
def compileCommandSequential(
files: NonEmptyList[Path],
log: Boolean,
outDir: Option[Path]
): IO[ExitCode] =
files
.traverse { file =>
compile(file.toAbsolutePath, outDir, log).handleErrorWith { err =>
// TODO: probably a more elegant way of doing this
// also, -1 arbitrary
// also - this outputs two messages for some reason
logger.error(err.getMessage) // *> IO.raiseError(err)
}
}
.map { exitCodes =>
if (exitCodes.exists(_ != 0)) ExitCode.Error else ExitCode.Success
}
def compileCommandParallel(
files: NonEmptyList[Path],
log: Boolean,
outDir: Option[Path]
): IO[ExitCode] =
files
.parTraverse { file =>
compile(file.toAbsolutePath, outDir, log).handleErrorWith { err =>
// TODO: probably a more elegant way of doing this
// also, -1 arbitrary
// also - this outputs two messages for some reason
logger.error(err.getMessage) // *> IO.raiseError(err)
}
}
.map { exitCodes =>
if (exitCodes.exists(_ != 0)) ExitCode.Error else ExitCode.Success
}
object Main
extends CommandIOApp(
name = "wacc",
header = "The ultimate WACC compiler",
version = "1.0"
) {
def main: Opts[IO[ExitCode]] =
(greedyOpt, filesOpt, logOpt, outputOpt).mapN { (greedy, files, log, outDir) =>
if (greedy) compileCommandSequential(files, log, outDir)
else compileCommandParallel(files, log, outDir)
}
}