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/ 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) } }