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