171 lines
5.4 KiB
Scala
171 lines
5.4 KiB
Scala
package wacc
|
|
|
|
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 org.typelevel.log4cats.slf4j.Slf4jLogger
|
|
import org.typelevel.log4cats.Logger
|
|
|
|
import assemblyIR as asm
|
|
import cats.data.ValidatedNel
|
|
import java.io.File
|
|
import cats.data.NonEmptySeq
|
|
|
|
/*
|
|
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)
|
|
*/
|
|
|
|
private val SUCCESS = ExitCode.Success.code
|
|
private val ERROR = ExitCode.Error.code
|
|
|
|
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.")
|
|
.validate("Must have permissions to create & access the output path") { path =>
|
|
try {
|
|
Files.createDirectories(path)
|
|
true
|
|
} catch {
|
|
case e: java.nio.file.AccessDeniedException =>
|
|
false
|
|
}
|
|
}
|
|
.validate("Output path must be a directory") { path =>
|
|
Files.isDirectory(path)
|
|
}
|
|
.orNone
|
|
|
|
def frontend(
|
|
contents: String,
|
|
file: File
|
|
): IO[Either[NonEmptySeq[Error], microWacc.Program]] =
|
|
parser.parse(contents) match {
|
|
case Failure(msg) => IO.pure(Left(NonEmptySeq.one(Error.SyntaxError(file, msg))))
|
|
case Success(fn) =>
|
|
val partialProg = fn(file)
|
|
|
|
for {
|
|
(typedProg, errors) <- semantics.check(partialProg)
|
|
res = NonEmptySeq.fromSeq(errors.iterator.toSeq).map(Left(_)).getOrElse(Right(typedProg))
|
|
} yield res
|
|
}
|
|
|
|
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)))
|
|
|
|
// 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] =
|
|
val backendStart = System.nanoTime()
|
|
val asmLines = backend(typedProg)
|
|
val backendEnd = System.nanoTime()
|
|
writer.writeTo(asmLines, outputPath) *>
|
|
logAction(
|
|
s"Backend time (${filePath.toRealPath()}): ${(backendEnd - backendStart).toFloat / 1e6} ms"
|
|
) *>
|
|
IO.blocking(println(s"Success: ${outputPath.toRealPath()}"))
|
|
|
|
def processProgram(contents: String, file: File, outDir: Path): IO[Int] =
|
|
val frontendStart = System.nanoTime()
|
|
for {
|
|
frontendResult <- frontend(contents, file)
|
|
frontendEnd = System.nanoTime()
|
|
_ <- logAction(
|
|
s"Frontend time (${filePath.toRealPath()}): ${(frontendEnd - frontendStart).toFloat / 1e6} ms"
|
|
)
|
|
res <- frontendResult match {
|
|
case Left(errors) =>
|
|
val code = errors.map(err => err.exitCode).toList.min
|
|
val errorMsg = errors.map(formatError).toIterable.mkString("\n")
|
|
for {
|
|
_ <- logAction(s"Compilation failed for $filePath\nExit code: $code")
|
|
_ <- IO.blocking(
|
|
// Explicit println since we want this to always show without logger thread info e.t.c.
|
|
println(s"Compilation failed for ${file.getCanonicalPath}:\n$errorMsg")
|
|
)
|
|
} yield code
|
|
|
|
case Right(typedProg) =>
|
|
val outputFile = outDir.resolve(filePath.getFileName.toString.stripSuffix(".wacc") + ".s")
|
|
writeOutputFile(typedProg, outputFile).as(SUCCESS)
|
|
}
|
|
} yield res
|
|
|
|
for {
|
|
contents <- readSourceFile
|
|
_ <- logAction(s"Compiling file: ${filePath.toAbsolutePath}")
|
|
exitCode <- processProgram(contents, filePath.toFile, outputDir.getOrElse(filePath.getParent))
|
|
} yield exitCode
|
|
}
|
|
|
|
def compileCommandParallel(
|
|
files: NonEmptyList[Path],
|
|
log: Boolean,
|
|
outDir: Option[Path]
|
|
): IO[ExitCode] =
|
|
files
|
|
.parTraverse { file => compile(file.toAbsolutePath, outDir, log) }
|
|
.map { exitCodes =>
|
|
exitCodes.filter(_ != 0) match {
|
|
case Nil => ExitCode.Success
|
|
case errorCodes => ExitCode(errorCodes.min)
|
|
}
|
|
}
|
|
|
|
object Main
|
|
extends CommandIOApp(
|
|
name = "wacc",
|
|
header = "The ultimate WACC compiler",
|
|
version = "1.0"
|
|
) {
|
|
def main: Opts[IO[ExitCode]] =
|
|
(filesOpt, logOpt, outputOpt).mapN { (files, log, outDir) =>
|
|
compileCommandParallel(files, log, outDir)
|
|
}
|
|
|
|
}
|