192 lines
6.2 KiB
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)
|
|
}
|
|
|
|
}
|