From 345c652a5755fd5c453f4c29a6a99d5e254942e2 Mon Sep 17 00:00:00 2001 From: Jonny Date: Fri, 28 Feb 2025 15:18:24 +0000 Subject: [PATCH 01/32] feat: introduce cats-effect and io --- project.scala | 21 ++++----- src/main/wacc/Main.scala | 85 ++++++++++++++++++------------------ src/test/wacc/examples.scala | 3 +- 3 files changed, 53 insertions(+), 56 deletions(-) diff --git a/project.scala b/project.scala index 4deaa08..6443ad8 100644 --- a/project.scala +++ b/project.scala @@ -5,23 +5,18 @@ //> using dep com.github.j-mie6::parsley::5.0.0-M10 //> using dep com.github.j-mie6::parsley-cats::1.5.0 //> using dep com.lihaoyi::os-lib::0.11.4 +//> using dep org.typelevel::cats-core::2.13.0 +//> using dep org.typelevel::cats-effect::3.5.7 +//> using dep com.monovore::decline::2.5.0 +//> using dep com.monovore::decline-effect::2.5.0 //> using dep com.github.scopt::scopt::4.1.0 //> using test.dep org.scalatest::scalatest::3.2.19 -// these are all sensible defaults to catch annoying issues +// sensible defaults for warnings and compiler checks //> using options -deprecation -unchecked -feature //> using options -Wimplausible-patterns -Wunused:all //> using options -Yexplicit-nulls -Wsafe-init -Xkind-projector:underscores -// these will help ensure you have access to the latest parsley releases -// even before they land on maven proper, or snapshot versions, if necessary. -// just in case they cause problems, however, keep them turned off unless you -// specifically need them. -// using repositories sonatype-s01:releases -// using repositories sonatype-s01:snapshots - -// these are flags used by Scala native: if you aren't using scala-native, then they do nothing -// lto-thin has decent linking times, and release-fast does not too much optimisation. -// using nativeLto thin -// using nativeGc commix -// using nativeMode release-fast +// repositories for pre-release versions if needed +//> using repositories sonatype-s01:releases +//> using repositories sonatype-s01:snapshots diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index fc9fb45..8bf7d3b 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -6,8 +6,12 @@ import parsley.{Failure, Success} import scopt.OParser import java.io.File import java.io.PrintStream +import cats.implicits._ +import cats.effect.unsafe.implicits.global import assemblyIR as asm +import cats.effect.IO +import cats.effect.IOApp case class CliConfig( file: File = new File(".") @@ -36,30 +40,26 @@ val cliParser = { def frontend( contents: String -)(using stdout: PrintStream): Either[microWacc.Program, Int] = { - parser.parse(contents) match { +)(using stdout: PrintStream): IO[microWacc.Program] = { + IO(parser.parse(contents)).flatMap { + case Failure(msg) => IO.raiseError(new RuntimeException(msg)) 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) - if (errors.result.nonEmpty) { - given errorContent: String = contents - Right( - errors.result - .map { error => - printError(error) - error match { - case _: Error.InternalError => 201 - case _ => 200 - } - } - .max() - ) - } else Left(typedProg) - case Failure(msg) => - stdout.println(msg) - Right(100) + given errors: mutable.Builder[Error, List[Error]] = List.newBuilder + given errorContent: String = contents + + val (names, funcs) = renamer.rename(prog) + given ctx: typeChecker.TypeCheckerCtx = typeChecker.TypeCheckerCtx(names, funcs, errors) + + val typedProg = typeChecker.check(prog) + + if (errors.result.isEmpty) IO.pure(typedProg) + else { + errors.result.foreach(printError) + IO.raiseError(new RuntimeException("Compilation failed with code: " + errors.result.view.map { + case _: Error.InternalError => 201 + case _ => 200 + }.max)) + } } } @@ -67,26 +67,27 @@ val s = "enter an integer to echo" def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = asmGenerator.generateAsm(typedProg) + def compile(filename: String, outFile: Option[File] = None)(using stdout: PrintStream = Console.out -): Int = - frontend(os.read(os.Path(filename))) match { - case Left(typedProg) => - val asmFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s")) - val asm = backend(typedProg) - writer.writeTo(asm, PrintStream(asmFile)) - 0 - case Right(exitCode) => exitCode - } - -def main(args: Array[String]): Unit = - OParser.parse(cliParser, args, CliConfig()) match { - case Some(config) => - System.exit( - compile( - config.file.getAbsolutePath, - outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s")) +): IO[Int] = + for { + contents <- IO(os.read(os.Path(filename))) + typedProg <- frontend(contents) + _ <- IO { + writer.writeTo( + backend(typedProg), + PrintStream(outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s"))) ) - ) - case None => } +} yield 0 + +object Main extends IOApp.Simple { + override def run: IO[Unit] = + OParser.parse(cliParser, sys.env.getOrElse("WACC_ARGS", "").split(" "), CliConfig()).traverse_ { config => + compile( + config.file.getAbsolutePath, + outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s")) + ) + } +} diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index 6114afd..4fac462 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -7,6 +7,7 @@ import java.io.File import sys.process._ import java.io.PrintStream import scala.io.Source +import cats.effect.unsafe.implicits.global class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll { val files = @@ -29,7 +30,7 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll { given stdout: PrintStream = PrintStream(File(baseFilename + ".out")) s"$filename" should "be compiled with correct result" in { - val result = compile(filename) + val result = compile(filename).unsafeRunSync() assert(expectedResult.contains(result)) } From cf1028454d69cd0f3087327c4eadb163d8701d54 Mon Sep 17 00:00:00 2001 From: Jonny Date: Fri, 28 Feb 2025 15:20:32 +0000 Subject: [PATCH 02/32] fix: fix frontend tests failing due to expecting error codes instead of runtime exceptions --- src/main/wacc/Main.scala | 57 ++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 8bf7d3b..71a65bb 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -40,26 +40,29 @@ val cliParser = { def frontend( contents: String -)(using stdout: PrintStream): IO[microWacc.Program] = { - IO(parser.parse(contents)).flatMap { - case Failure(msg) => IO.raiseError(new RuntimeException(msg)) +)(using stdout: PrintStream): IO[Either[Int, microWacc.Program]] = { + IO(parser.parse(contents)).map { + case Failure(msg) => + stdout.println(msg) + Left(100) // Syntax error + case Success(prog) => - given errors: mutable.Builder[Error, List[Error]] = List.newBuilder - given errorContent: String = contents + given errors: mutable.Builder[Error, List[Error]] = List.newBuilder + given errorContent: String = contents - val (names, funcs) = renamer.rename(prog) - given ctx: typeChecker.TypeCheckerCtx = typeChecker.TypeCheckerCtx(names, funcs, errors) + val (names, funcs) = renamer.rename(prog) + given ctx: typeChecker.TypeCheckerCtx = typeChecker.TypeCheckerCtx(names, funcs, errors) - val typedProg = typeChecker.check(prog) + val typedProg = typeChecker.check(prog) - if (errors.result.isEmpty) IO.pure(typedProg) - else { - errors.result.foreach(printError) - IO.raiseError(new RuntimeException("Compilation failed with code: " + errors.result.view.map { - case _: Error.InternalError => 201 - case _ => 200 - }.max)) - } + if (errors.result.isEmpty) Right(typedProg) + else { + errors.result.foreach(printError) + Left(errors.result.view.map { + case _: Error.InternalError => 201 + case _ => 200 + }.max) + } } } @@ -72,15 +75,19 @@ def compile(filename: String, outFile: Option[File] = None)(using stdout: PrintStream = Console.out ): IO[Int] = for { - contents <- IO(os.read(os.Path(filename))) - typedProg <- frontend(contents) - _ <- IO { - writer.writeTo( - backend(typedProg), - PrintStream(outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s"))) - ) - } -} yield 0 + contents <- IO(os.read(os.Path(filename))) + result <- frontend(contents) + exitCode <- result match { + case Left(code) => IO.pure(code) // Return error code + case Right(typedProg) => + IO { + writer.writeTo( + backend(typedProg), + PrintStream(outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s"))) + ) + }.as(0) // Compilation succeeded + } + } yield exitCode object Main extends IOApp.Simple { override def run: IO[Unit] = From e54e5ce15158715e211aa37fca6802d5b0d90eb5 Mon Sep 17 00:00:00 2001 From: Jonny Date: Fri, 28 Feb 2025 15:50:53 +0000 Subject: [PATCH 03/32] refactor: style fixes and fold combinator used instead of explicit pattern match --- src/main/wacc/Main.scala | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 71a65bb..4d3c101 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -6,8 +6,7 @@ import parsley.{Failure, Success} import scopt.OParser import java.io.File import java.io.PrintStream -import cats.implicits._ -import cats.effect.unsafe.implicits.global +import cats.implicits.* import assemblyIR as asm import cats.effect.IO @@ -70,31 +69,31 @@ val s = "enter an integer to echo" def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = asmGenerator.generateAsm(typedProg) - def compile(filename: String, outFile: Option[File] = None)(using stdout: PrintStream = Console.out ): IO[Int] = for { contents <- IO(os.read(os.Path(filename))) result <- frontend(contents) - exitCode <- result match { - case Left(code) => IO.pure(code) // Return error code - case Right(typedProg) => + exitCode <- result.fold( + IO.pure, // Return error code (handles Left case) + typedProg => IO { writer.writeTo( backend(typedProg), PrintStream(outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s"))) ) }.as(0) // Compilation succeeded - } + ) } yield exitCode object Main extends IOApp.Simple { override def run: IO[Unit] = - OParser.parse(cliParser, sys.env.getOrElse("WACC_ARGS", "").split(" "), CliConfig()).traverse_ { config => - compile( - config.file.getAbsolutePath, - outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s")) - ) - } + OParser.parse(cliParser, sys.env.getOrElse("WACC_ARGS", "").split(" "), CliConfig()).traverse_ { + config => + compile( + config.file.getAbsolutePath, + outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s")) + ) + } } From 1a72decf557820f81281db768603c1a69ff82b90 Mon Sep 17 00:00:00 2001 From: Jonny Date: Fri, 28 Feb 2025 16:24:53 +0000 Subject: [PATCH 04/32] feat: remove unsaferunsync and integrate io in tests instead --- project.scala | 2 + src/test/wacc/examples.scala | 157 ++++++++++++++++++----------------- 2 files changed, 83 insertions(+), 76 deletions(-) diff --git a/project.scala b/project.scala index 6443ad8..86e8a44 100644 --- a/project.scala +++ b/project.scala @@ -11,6 +11,8 @@ //> using dep com.monovore::decline-effect::2.5.0 //> using dep com.github.scopt::scopt::4.1.0 //> using test.dep org.scalatest::scalatest::3.2.19 +//> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0 + // sensible defaults for warnings and compiler checks //> using options -deprecation -unchecked -feature diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index 4fac462..cd79161 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -1,15 +1,19 @@ package wacc import org.scalatest.BeforeAndAfterAll -import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.Inspectors.forEvery +import org.scalatest.matchers.should.Matchers._ +import org.scalatest.freespec.AsyncFreeSpec +import cats.effect.testing.scalatest.AsyncIOSpec import java.io.File import sys.process._ import java.io.PrintStream import scala.io.Source -import cats.effect.unsafe.implicits.global +import cats.effect.IO +import wacc.{compile as compileWacc} + +class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAndAfterAll { -class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll { val files = allWaccFiles("wacc-examples/valid").map { p => (p.toString, List(0)) @@ -24,95 +28,96 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll { (p.toString, List(100, 200)) } - // tests go here forEvery(files) { (filename, expectedResult) => val baseFilename = filename.stripSuffix(".wacc") given stdout: PrintStream = PrintStream(File(baseFilename + ".out")) - s"$filename" should "be compiled with correct result" in { - val result = compile(filename).unsafeRunSync() - assert(expectedResult.contains(result)) - } + s"$filename" - { + "should be compiled with correct result" in { + compileWacc(filename).map { result => + expectedResult should contain(result) + } + } - if (expectedResult == List(0)) it should "run with correct result" in { - if (fileIsDisallowedBackend(filename)) pending + if (expectedResult == List(0)) { + "should run with correct result" in { + if (fileIsDisallowedBackend(filename)) + IO.pure( + succeed + ) // TODO: remove when advanced tests removed. not sure how to "pending" this otherwise + else { + for { + contents <- IO(Source.fromFile(File(filename)).getLines.toList) + inputLine = extractInput(contents) + expectedOutput = extractOutput(contents) + expectedExit = extractExit(contents) - // Retrieve contents to get input and expected output + exit code - val contents = scala.io.Source.fromFile(File(filename)).getLines.toList - val inputLine = - contents - .find(_.matches("^# ?[Ii]nput:.*$")) - .map(_.split(":").last.strip + "\n") - .getOrElse("") - val outputLineIdx = contents.indexWhere(_.matches("^# ?[Oo]utput:.*$")) - val expectedOutput = - if (outputLineIdx == -1) "" - else - contents - .drop(outputLineIdx + 1) - .takeWhile(_.startsWith("#")) - .map(_.stripPrefix("#").stripLeading) - .mkString("\n") + asmFilename = baseFilename + ".s" + execFilename = baseFilename + gccResult <- IO(s"gcc -o $execFilename -z noexecstack $asmFilename".!) - val exitLineIdx = contents.indexWhere(_.matches("^# ?[Ee]xit:.*$")) - val expectedExit = - if (exitLineIdx == -1) 0 - else contents(exitLineIdx + 1).stripPrefix("#").strip.toInt + _ = assert(gccResult == 0) - // Assembly and link using gcc - val asmFilename = baseFilename + ".s" - val execFilename = baseFilename - val gccResult = s"gcc -o $execFilename -z noexecstack $asmFilename".! - assert(gccResult == 0) + stdout <- IO.pure(new StringBuilder) + process <- IO { + s"timeout 5s $execFilename" run ProcessIO( + in = w => { + w.write(inputLine.getBytes) + w.close() + }, + out = Source.fromInputStream(_).addString(stdout), + err = _ => () + ) + } - // Run the executable with the provided input - val stdout = new StringBuilder - val process = s"timeout 5s $execFilename" run ProcessIO( - in = w => { - w.write(inputLine.getBytes) - w.close() - }, - out = Source.fromInputStream(_).addString(stdout), - err = _ => () - ) + exitCode <- IO.pure(process.exitValue) - assert(process.exitValue == expectedExit) - assert( - stdout.toString - .replaceAll("0x[0-9a-f]+", "#addrs#") - .replaceAll("fatal error:.*", "#runtime_error#\u0000") - .takeWhile(_ != '\u0000') - == expectedOutput - ) + } yield { + exitCode shouldBe expectedExit + normalizeOutput(stdout.toString) shouldBe expectedOutput + } + } + } + } } } def allWaccFiles(dir: String): IndexedSeq[os.Path] = val d = java.io.File(dir) - os.walk(os.Path(d.getAbsolutePath)).filter { _.ext == "wacc" } + os.walk(os.Path(d.getAbsolutePath)).filter(_.ext == "wacc") + // TODO: eventually remove this I think def fileIsDisallowedBackend(filename: String): Boolean = Seq( - // format: off - // disable formatting to avoid binPack - "^.*wacc-examples/valid/advanced.*$", - // "^.*wacc-examples/valid/array.*$", - // "^.*wacc-examples/valid/basic/exit.*$", - // "^.*wacc-examples/valid/basic/skip.*$", - // "^.*wacc-examples/valid/expressions.*$", - // "^.*wacc-examples/valid/function/nested_functions.*$", - // "^.*wacc-examples/valid/function/simple_functions.*$", - // "^.*wacc-examples/valid/if.*$", - // "^.*wacc-examples/valid/IO/print.*$", - // "^.*wacc-examples/valid/IO/read.*$", - // "^.*wacc-examples/valid/IO/IOLoop.wacc.*$", - // "^.*wacc-examples/valid/IO/IOSequence.wacc.*$", - // "^.*wacc-examples/valid/pairs.*$", - //"^.*wacc-examples/valid/runtimeErr.*$", - // "^.*wacc-examples/valid/scope.*$", - // "^.*wacc-examples/valid/sequence.*$", - // "^.*wacc-examples/valid/variables.*$", - // "^.*wacc-examples/valid/while.*$", - // format: on - ).find(filename.matches).isDefined + "^.*wacc-examples/valid/advanced.*$" + ).exists(filename.matches) + + private def extractInput(contents: List[String]): String = + contents + .find(_.matches("^# ?[Ii]nput:.*$")) + .map(_.split(":").last.strip + "\n") + .getOrElse("") + + private def extractOutput(contents: List[String]): String = { + val outputLineIdx = contents.indexWhere(_.matches("^# ?[Oo]utput:.*$")) + if (outputLineIdx == -1) "" + else + contents + .drop(outputLineIdx + 1) + .takeWhile(_.startsWith("#")) + .map(_.stripPrefix("#").stripLeading) + .mkString("\n") + } + + private def extractExit(contents: List[String]): Int = { + val exitLineIdx = contents.indexWhere(_.matches("^# ?[Ee]xit:.*$")) + if (exitLineIdx == -1) 0 + else contents(exitLineIdx + 1).stripPrefix("#").strip.toInt + } + + private def normalizeOutput(output: String): String = + output + .replaceAll("0x[0-9a-f]+", "#addrs#") + .replaceAll("fatal error:.*", "#runtime_error#\u0000") + .takeWhile(_ != '\u0000') } From d56be9249a07893769f288484637fd3d65c75ae3 Mon Sep 17 00:00:00 2001 From: Jonny Date: Fri, 28 Feb 2025 18:00:18 +0000 Subject: [PATCH 05/32] refactor: introduce decline to integrate command-line parsing with cats-effect --- project.scala | 1 - src/main/wacc/Main.scala | 65 +++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/project.scala b/project.scala index 86e8a44..b6fb20a 100644 --- a/project.scala +++ b/project.scala @@ -9,7 +9,6 @@ //> using dep org.typelevel::cats-effect::3.5.7 //> using dep com.monovore::decline::2.5.0 //> using dep com.monovore::decline-effect::2.5.0 -//> using dep com.github.scopt::scopt::4.1.0 //> using test.dep org.scalatest::scalatest::3.2.19 //> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0 diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 4d3c101..ef4575c 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -3,40 +3,40 @@ package wacc import scala.collection.mutable import cats.data.Chain import parsley.{Failure, Success} -import scopt.OParser import java.io.File import java.io.PrintStream import cats.implicits.* -import assemblyIR as asm import cats.effect.IO -import cats.effect.IOApp +import cats.effect.ExitCode + +import com.monovore.decline._ +import com.monovore.decline.effect._ +import com.monovore.decline.Argument + +import assemblyIR as asm + case class CliConfig( file: File = new File(".") ) -val cliBuilder = OParser.builder[CliConfig] -val cliParser = { - import cliBuilder._ - OParser.sequence( - programName("wacc-compiler"), - help('h', "help") - .text("Prints this help message"), - arg[File]("") - .text("Input WACC source file") - .required() - .action((f, c) => c.copy(file = f)) - .validate(f => - if (!f.exists) failure("File does not exist") - else if (!f.isFile) failure("File must be a regular file") - else if (!f.getName.endsWith(".wacc")) - failure("File must have .wacc extension") - else success - ) - ) +given Argument[File] = Argument.from("file") { str => + val file = File(str) + ( + Option.when(file.exists())(file).toValidNel(s"File '${file.getAbsolutePath}' does not exist"), + Option + .when(file.isFile())(file) + .toValidNel(s"File '${file.getAbsolutePath}' must be a regular file"), + Option.when(file.getName.endsWith(".wacc"))(file).toValidNel("File must have .wacc extension") + ).mapN((_, _, _) => file) } +val cliCommand: Command[File] = + Command("wacc-compiler", "Compile WACC programs") { + Opts.argument[File]("file") + } + def frontend( contents: String )(using stdout: PrintStream): IO[Either[Int, microWacc.Program]] = { @@ -87,13 +87,18 @@ def compile(filename: String, outFile: Option[File] = None)(using ) } yield exitCode -object Main extends IOApp.Simple { - override def run: IO[Unit] = - OParser.parse(cliParser, sys.env.getOrElse("WACC_ARGS", "").split(" "), CliConfig()).traverse_ { - config => - compile( - config.file.getAbsolutePath, - outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s")) - ) +object Main + extends CommandIOApp( + name = "wacc-compiler", + header = "the ultimate wacc compiler", + version = "1.0" + ) { + def main: Opts[IO[ExitCode]] = + Opts.argument[File]("file").map { file => + compile( + file.getAbsolutePath, + outFile = Some(File(".", file.getName.stripSuffix(".wacc") + ".s")) + ).map(ExitCode(_)) // turn the int into exit code for compatibility with commandioapp + // https://ben.kirw.in/decline/effect.html } } From d214723f3560544227ca6578c75f2fc3b8f52000 Mon Sep 17 00:00:00 2001 From: Jonny Date: Fri, 28 Feb 2025 19:36:22 +0000 Subject: [PATCH 06/32] feat: parallelise compilation of multiple files given to cli --- src/main/wacc/Main.scala | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index ef4575c..35479bd 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -16,11 +16,6 @@ import com.monovore.decline.Argument import assemblyIR as asm - -case class CliConfig( - file: File = new File(".") -) - given Argument[File] = Argument.from("file") { str => val file = File(str) ( @@ -94,11 +89,14 @@ object Main version = "1.0" ) { def main: Opts[IO[ExitCode]] = - Opts.argument[File]("file").map { file => - compile( - file.getAbsolutePath, - outFile = Some(File(".", file.getName.stripSuffix(".wacc") + ".s")) - ).map(ExitCode(_)) // turn the int into exit code for compatibility with commandioapp - // https://ben.kirw.in/decline/effect.html + Opts.arguments[File]("files").map { files => + files + .parTraverse_ { file => + compile( + file.getAbsolutePath, + outFile = Some(File(".", file.getName.stripSuffix(".wacc") + ".s")) + ) + } + .as(ExitCode.Success) } } From 667fbf4949bdf553e271797d9933331aa002a9bd Mon Sep 17 00:00:00 2001 From: Jonny Date: Sat, 1 Mar 2025 01:19:50 +0000 Subject: [PATCH 07/32] feat: introduction of logger to eliminate printstreams --- project.scala | 2 ++ src/main/wacc/Main.scala | 42 ++++++++++++++++-------------- src/main/wacc/backend/writer.scala | 23 +++++++++++++--- src/test/wacc/examples.scala | 2 -- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/project.scala b/project.scala index b6fb20a..e4f1c0f 100644 --- a/project.scala +++ b/project.scala @@ -9,6 +9,8 @@ //> using dep org.typelevel::cats-effect::3.5.7 //> using dep com.monovore::decline::2.5.0 //> using dep com.monovore::decline-effect::2.5.0 +//> using dep org.typelevel::log4cats-slf4j::2.7.0 +//> using dep org.slf4j:slf4j-simple:2.0.17 //> using test.dep org.scalatest::scalatest::3.2.19 //> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0 diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 35479bd..20acd19 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -4,7 +4,6 @@ import scala.collection.mutable import cats.data.Chain import parsley.{Failure, Success} import java.io.File -import java.io.PrintStream import cats.implicits.* import cats.effect.IO @@ -14,6 +13,9 @@ 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 given Argument[File] = Argument.from("file") { str => @@ -32,30 +34,33 @@ val cliCommand: Command[File] = Opts.argument[File]("file") } +given logger: Logger[IO] = Slf4jLogger.getLogger[IO] + def frontend( contents: String -)(using stdout: PrintStream): IO[Either[Int, microWacc.Program]] = { - IO(parser.parse(contents)).map { +): IO[Either[Int, microWacc.Program]] = { + IO(parser.parse(contents)).flatMap { case Failure(msg) => - stdout.println(msg) - Left(100) // Syntax error + logger.error(s"Syntax error: $msg").as(Left(100)) case Success(prog) => given errors: mutable.Builder[Error, List[Error]] = List.newBuilder - given errorContent: String = contents val (names, funcs) = renamer.rename(prog) given ctx: typeChecker.TypeCheckerCtx = typeChecker.TypeCheckerCtx(names, funcs, errors) val typedProg = typeChecker.check(prog) - if (errors.result.isEmpty) Right(typedProg) + if (errors.result.isEmpty) IO.pure(Right(typedProg)) else { - errors.result.foreach(printError) - Left(errors.result.view.map { + val exitCode = errors.result.view.map { case _: Error.InternalError => 201 case _ => 200 - }.max) + }.max + + logger.error(s"Semantic errors:\n${errors.result.mkString("\n")}") *> IO.pure( + Left(exitCode) + ) } } } @@ -64,21 +69,18 @@ val s = "enter an integer to echo" def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = asmGenerator.generateAsm(typedProg) -def compile(filename: String, outFile: Option[File] = None)(using - stdout: PrintStream = Console.out -): IO[Int] = +def compile(filename: String, outFile: Option[File] = None): IO[Int] = for { contents <- IO(os.read(os.Path(filename))) + _ <- logger.info(s"Compiling file: $filename") result <- frontend(contents) exitCode <- result.fold( - IO.pure, // Return error code (handles Left case) + code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), typedProg => - IO { - writer.writeTo( - backend(typedProg), - PrintStream(outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s"))) - ) - }.as(0) // Compilation succeeded + val outputFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s")) + writer.writeTo(backend(typedProg), outputFile) *> logger + .info(s"Compilation succeeded: $filename") + .as(0) ) } yield exitCode diff --git a/src/main/wacc/backend/writer.scala b/src/main/wacc/backend/writer.scala index 3c8dcfd..b87ec01 100644 --- a/src/main/wacc/backend/writer.scala +++ b/src/main/wacc/backend/writer.scala @@ -1,12 +1,27 @@ package wacc -import java.io.PrintStream +import cats.effect.Resource +import java.nio.charset.StandardCharsets +import java.io.File +import java.io.BufferedWriter +import java.io.FileWriter import cats.data.Chain +import cats.effect.IO + +import org.typelevel.log4cats.Logger object writer { import assemblyIR._ - def writeTo(asmList: Chain[AsmLine], printStream: PrintStream): Unit = { - asmList.iterator.foreach(printStream.println) - } + def writeTo(asmList: Chain[AsmLine], outputFile: File)(using logger: Logger[IO]): IO[Unit] = + Resource + .fromAutoCloseable { + IO(BufferedWriter(FileWriter(outputFile, StandardCharsets.UTF_8))) + } + .use { writer => + IO { + asmList.iterator.foreach(line => writer.write(line.toString + "\n")) + writer.flush() // TODO: NECESSARY OR NOT? + } *> logger.info(s"Wrote assembly to ${outputFile.getAbsolutePath}") + } } diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index cd79161..76e84ed 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -7,7 +7,6 @@ import org.scalatest.freespec.AsyncFreeSpec import cats.effect.testing.scalatest.AsyncIOSpec import java.io.File import sys.process._ -import java.io.PrintStream import scala.io.Source import cats.effect.IO import wacc.{compile as compileWacc} @@ -30,7 +29,6 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd forEvery(files) { (filename, expectedResult) => val baseFilename = filename.stripSuffix(".wacc") - given stdout: PrintStream = PrintStream(File(baseFilename + ".out")) s"$filename" - { "should be compiled with correct result" in { From 01b38b14455ad3c48437d2a941c943adf25ac28e Mon Sep 17 00:00:00 2001 From: Jonny Date: Sat, 1 Mar 2025 01:34:05 +0000 Subject: [PATCH 08/32] fix: fix incorrect semantic error logging by refactoring error.scala from frontend --- src/main/wacc/Main.scala | 7 +- src/main/wacc/frontend/Error.scala | 132 ++++++++++++++--------------- 2 files changed, 70 insertions(+), 69 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 20acd19..ad43479 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -53,14 +53,15 @@ def frontend( if (errors.result.isEmpty) IO.pure(Right(typedProg)) else { + given errorContent: String = contents val exitCode = errors.result.view.map { case _: Error.InternalError => 201 case _ => 200 }.max - logger.error(s"Semantic errors:\n${errors.result.mkString("\n")}") *> IO.pure( - Left(exitCode) - ) + val formattedErrors = errors.result.map(formatError).mkString("\n") + + logger.error(s"Semantic errors:\n$formattedErrors") *> IO.pure(Left(exitCode)) } } } diff --git a/src/main/wacc/frontend/Error.scala b/src/main/wacc/frontend/Error.scala index 9c02a60..b991421 100644 --- a/src/main/wacc/frontend/Error.scala +++ b/src/main/wacc/frontend/Error.scala @@ -2,7 +2,6 @@ package wacc import wacc.ast.Position import wacc.types._ -import java.io.PrintStream /** Error types for semantic errors */ @@ -24,71 +23,72 @@ enum Error { * @param errorContent * Contents of the file to generate code snippets */ -def printError(error: Error)(using errorContent: String, stdout: PrintStream): Unit = { - stdout.println("Semantic error:") - error match { - case Error.DuplicateDeclaration(ident) => - printPosition(ident.pos) - stdout.println(s"Duplicate declaration of identifier ${ident.v}") - highlight(ident.pos, ident.v.length) - case Error.UndeclaredVariable(ident) => - printPosition(ident.pos) - stdout.println(s"Undeclared variable ${ident.v}") - highlight(ident.pos, ident.v.length) - case Error.UndefinedFunction(ident) => - printPosition(ident.pos) - stdout.println(s"Undefined function ${ident.v}") - highlight(ident.pos, ident.v.length) - case Error.FunctionParamsMismatch(id, expected, got, funcType) => - printPosition(id.pos) - stdout.println(s"Function expects $expected parameters, got $got") - stdout.println( - s"(function ${id.v} has type (${funcType.params.mkString(", ")}) -> ${funcType.returnType})" - ) - highlight(id.pos, 1) - case Error.TypeMismatch(pos, expected, got, msg) => - printPosition(pos) - stdout.println(s"Type mismatch: $msg\nExpected: $expected\nGot: $got") - highlight(pos, 1) - case Error.SemanticError(pos, msg) => - printPosition(pos) - stdout.println(msg) - highlight(pos, 1) - case wacc.Error.InternalError(pos, msg) => - printPosition(pos) - stdout.println(s"Internal error: $msg") - highlight(pos, 1) +def formatError(error: Error)(using errorContent: String): String = { + val sb = new StringBuilder() + sb.append("Semantic error:\n") + + /** Function to format the position of an error + * + * @param pos + * Position of the error + */ + def formatPosition(pos: Position): Unit = { + sb.append(s"(line ${pos.line}, column ${pos.column}):\n") } -} - -/** Function to highlight a section of code for an error message - * - * @param pos - * Position of the error - * @param size - * Size(in chars) of section to highlight - * @param errorContent - * Contents of the file to generate code snippets - */ -def highlight(pos: Position, size: Int)(using errorContent: String, stdout: PrintStream): Unit = { - val lines = errorContent.split("\n") - - val preLine = if (pos.line > 1) lines(pos.line - 2) else "" - val midLine = lines(pos.line - 1) - val postLine = if (pos.line < lines.size) lines(pos.line) else "" - val linePointer = " " * (pos.column + 2) + ("^" * (size)) + "\n" - - stdout.println( - s" >$preLine\n >$midLine\n$linePointer >$postLine" - ) -} - -/** Function to print the position of an error - * - * @param pos - * Position of the error - */ -def printPosition(pos: Position)(using stdout: PrintStream): Unit = { - stdout.println(s"(line ${pos.line}, column ${pos.column}):") + /** Function to highlight a section of code for an error message + * + * @param pos + * Position of the error + * @param size + * Size(in chars) of section to highlight + */ + def formatHighlight(pos: Position, size: Int): Unit = { + val lines = errorContent.split("\n") + val preLine = if (pos.line > 1) lines(pos.line - 2) else "" + val midLine = lines(pos.line - 1) + val postLine = if (pos.line < lines.size) lines(pos.line) else "" + val linePointer = " " * (pos.column + 2) + ("^" * (size)) + "\n" + + sb.append( + s" >$preLine\n >$midLine\n$linePointer >$postLine\netscape" + ) + } + + error match { + case Error.DuplicateDeclaration(ident) => + formatPosition(ident.pos) + sb.append(s"Duplicate declaration of identifier ${ident.v}") + formatHighlight(ident.pos, ident.v.length) + case Error.UndeclaredVariable(ident) => + formatPosition(ident.pos) + sb.append(s"Undeclared variable ${ident.v}") + formatHighlight(ident.pos, ident.v.length) + case Error.UndefinedFunction(ident) => + formatPosition(ident.pos) + sb.append(s"Undefined function ${ident.v}") + formatHighlight(ident.pos, ident.v.length) + case Error.FunctionParamsMismatch(id, expected, got, funcType) => + formatPosition(id.pos) + sb.append(s"Function expects $expected parameters, got $got") + sb.append( + s"(function ${id.v} has type (${funcType.params.mkString(", ")}) -> ${funcType.returnType})" + ) + formatHighlight(id.pos, 1) + case Error.TypeMismatch(pos, expected, got, msg) => + formatPosition(pos) + sb.append(s"Type mismatch: $msg\nExpected: $expected\nGot: $got") + formatHighlight(pos, 1) + case Error.SemanticError(pos, msg) => + formatPosition(pos) + sb.append(msg) + formatHighlight(pos, 1) + case wacc.Error.InternalError(pos, msg) => + formatPosition(pos) + sb.append(s"Internal error: $msg") + formatHighlight(pos, 1) + } + + sb.toString() + } From 85a82aabb4fd2e20a058273ba1f035c5480541f1 Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 2 Mar 2025 03:12:53 +0000 Subject: [PATCH 09/32] feat: add option flag, greedy compilation of multiple files, and refactor to use paths instead of files --- project.scala | 1 - src/main/wacc/Main.scala | 111 ++++++++++++++++++----------- src/main/wacc/backend/writer.scala | 8 +-- src/test/wacc/examples.scala | 2 +- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/project.scala b/project.scala index e4f1c0f..d8b5880 100644 --- a/project.scala +++ b/project.scala @@ -14,7 +14,6 @@ //> using test.dep org.scalatest::scalatest::3.2.19 //> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0 - // sensible defaults for warnings and compiler checks //> using options -deprecation -unchecked -feature //> using options -Wimplausible-patterns -Wunused:all diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index ad43479..f4c290b 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -1,10 +1,11 @@ package wacc import scala.collection.mutable -import cats.data.Chain +import cats.data.{Chain, NonEmptyList} import parsley.{Failure, Success} -import java.io.File -import cats.implicits.* + +import java.nio.file.{Files, Path, Paths} +import cats.syntax.all._ import cats.effect.IO import cats.effect.ExitCode @@ -18,24 +19,30 @@ import org.typelevel.log4cats.Logger import assemblyIR as asm -given Argument[File] = Argument.from("file") { str => - val file = File(str) + +// TODO: IO correctness, --greedy, parallelisable, and probably splitting this file up + + +given Argument[Path] = Argument.from("path") { str => + val path = Path.of(str) ( - Option.when(file.exists())(file).toValidNel(s"File '${file.getAbsolutePath}' does not exist"), - Option - .when(file.isFile())(file) - .toValidNel(s"File '${file.getAbsolutePath}' must be a regular file"), - Option.when(file.getName.endsWith(".wacc"))(file).toValidNel("File must have .wacc extension") - ).mapN((_, _, _) => file) + Either.cond(Files.exists(path), path, s"File '${path.toAbsolutePath}' does not exist"), + Either.cond(Files.isRegularFile(path), path, s"File '${path.toAbsolutePath}' must be a regular file"), + Either.cond(path.toString.endsWith(".wacc"), path, "File must have .wacc extension") + ).mapN((_, _, _) => path).toValidatedNel } -val cliCommand: Command[File] = - Command("wacc-compiler", "Compile WACC programs") { - Opts.argument[File]("file") - } - given logger: Logger[IO] = Slf4jLogger.getLogger[IO] +val logOpt: Opts[Boolean] = + Opts.flag("log", "Enable logging for additional compilation details", short = "l").orFalse + +val outputOpt: Opts[Option[Path]] = + Opts.option[Path]("output", "Specify path for output assembly file(s)").orNone + +val filesOpt: Opts[NonEmptyList[Path]] = Opts.arguments[Path]("files") + + def frontend( contents: String ): IO[Either[Int, microWacc.Program]] = { @@ -51,55 +58,73 @@ def frontend( val typedProg = typeChecker.check(prog) - if (errors.result.isEmpty) IO.pure(Right(typedProg)) + 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 = errors.result.view.map { + val exitCode = errResult.collectFirst { case _: Error.InternalError => 201 - case _ => 200 - }.max + }.getOrElse(200) - val formattedErrors = errors.result.map(formatError).mkString("\n") + val formattedErrors = errResult.map(formatError).mkString("\n") - logger.error(s"Semantic errors:\n$formattedErrors") *> IO.pure(Left(exitCode)) + logger.error(s"Semantic errors:\n$formattedErrors").as(Left(exitCode)) } } } -val s = "enter an integer to echo" def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = asmGenerator.generateAsm(typedProg) -def compile(filename: String, outFile: Option[File] = None): IO[Int] = +// TODO: filename being String seems unnatural due to Path refactor +// TODO: this function is doing too much should refactor +def compile(filename: String, outputDir: Option[Path], log: Boolean): IO[Int] = + val logAction: String => IO[Unit] = + if (log) logger.info(_) + else (_ => IO.unit) for { - contents <- IO(os.read(os.Path(filename))) - _ <- logger.info(s"Compiling file: $filename") + contents <- IO.delay(os.read(os.Path(filename))) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? + _ <- logAction(s"Compiling file: $filename") result <- frontend(contents) exitCode <- result.fold( code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), typedProg => - val outputFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s")) - writer.writeTo(backend(typedProg), outputFile) *> logger - .info(s"Compilation succeeded: $filename") - .as(0) + val outDir = outputDir.getOrElse(Paths.get(filename).getParent) + IO.delay(Files.createDirectories(outDir)) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? + val outputFile = outDir.resolve(filename.stripSuffix(".wacc") + ".s") + writer.writeTo(backend(typedProg), outputFile) *> // TODO: I dont think we need IO here if we look at the implementation of writer + logAction(s"Compilation succeeded: $filename").as(0) ) } yield exitCode +// TODO: this is sequential, thus should be what occurs when --greedy is passed in +val compileCommand: Opts[IO[ExitCode]] = + (filesOpt, logOpt, outputOpt).mapN{ + (files, log, outDir) => + files + .traverse{ file => + compile( + file.toAbsolutePath.toString, + outDir, + log) + }.map { + exitCodes => + if (exitCodes.exists(_ != 0)) ExitCode.Error // TODO- it should be the first one to exit when parallelised :) + else ExitCode.Success + } + + } + +// TODO: add parallelisable option object Main extends CommandIOApp( - name = "wacc-compiler", - header = "the ultimate wacc compiler", + name = "wacc", + header = "The ultimate WACC compiler", version = "1.0" ) { def main: Opts[IO[ExitCode]] = - Opts.arguments[File]("files").map { files => - files - .parTraverse_ { file => - compile( - file.getAbsolutePath, - outFile = Some(File(".", file.getName.stripSuffix(".wacc") + ".s")) - ) - } - .as(ExitCode.Success) - } -} + compileCommand + +} \ No newline at end of file diff --git a/src/main/wacc/backend/writer.scala b/src/main/wacc/backend/writer.scala index b87ec01..8789835 100644 --- a/src/main/wacc/backend/writer.scala +++ b/src/main/wacc/backend/writer.scala @@ -2,26 +2,26 @@ package wacc import cats.effect.Resource import java.nio.charset.StandardCharsets -import java.io.File import java.io.BufferedWriter import java.io.FileWriter import cats.data.Chain import cats.effect.IO import org.typelevel.log4cats.Logger +import java.nio.file.Path object writer { import assemblyIR._ - def writeTo(asmList: Chain[AsmLine], outputFile: File)(using logger: Logger[IO]): IO[Unit] = + def writeTo(asmList: Chain[AsmLine], outputPath: Path)(using logger: Logger[IO]): IO[Unit] = Resource .fromAutoCloseable { - IO(BufferedWriter(FileWriter(outputFile, StandardCharsets.UTF_8))) + IO(BufferedWriter(FileWriter(outputPath.toFile, StandardCharsets.UTF_8))) } .use { writer => IO { asmList.iterator.foreach(line => writer.write(line.toString + "\n")) writer.flush() // TODO: NECESSARY OR NOT? - } *> logger.info(s"Wrote assembly to ${outputFile.getAbsolutePath}") + } *> logger.info(s"Success: ${outputPath.toAbsolutePath}") } } diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index 76e84ed..ca1fe9d 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -32,7 +32,7 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd s"$filename" - { "should be compiled with correct result" in { - compileWacc(filename).map { result => + compileWacc(filename, outputDir = None, log = false).map { result => expectedResult should contain(result) } } From 9a5ccea1f610ae583b783a49a6c25bc04161d301 Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 2 Mar 2025 03:14:58 +0000 Subject: [PATCH 10/32] style: fix formatting --- src/main/wacc/Main.scala | 57 ++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index f4c290b..7afdd4f 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -19,15 +19,17 @@ import org.typelevel.log4cats.Logger import assemblyIR as asm - // TODO: IO correctness, --greedy, parallelisable, and probably splitting this file up - given Argument[Path] = Argument.from("path") { str => val path = Path.of(str) ( Either.cond(Files.exists(path), path, s"File '${path.toAbsolutePath}' does not exist"), - Either.cond(Files.isRegularFile(path), path, s"File '${path.toAbsolutePath}' must be a regular file"), + Either.cond( + Files.isRegularFile(path), + path, + s"File '${path.toAbsolutePath}' must be a regular file" + ), Either.cond(path.toString.endsWith(".wacc"), path, "File must have .wacc extension") ).mapN((_, _, _) => path).toValidatedNel } @@ -42,7 +44,6 @@ val outputOpt: Opts[Option[Path]] = val filesOpt: Opts[NonEmptyList[Path]] = Opts.arguments[Path]("files") - def frontend( contents: String ): IO[Either[Int, microWacc.Program]] = { @@ -64,9 +65,11 @@ def frontend( 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 exitCode = errResult + .collectFirst { case _: Error.InternalError => + 201 + } + .getOrElse(200) val formattedErrors = errResult.map(formatError).mkString("\n") @@ -85,35 +88,39 @@ def compile(filename: String, outputDir: Option[Path], log: Boolean): IO[Int] = if (log) logger.info(_) else (_ => IO.unit) for { - contents <- IO.delay(os.read(os.Path(filename))) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? + contents <- IO.delay( + os.read(os.Path(filename)) + ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? _ <- logAction(s"Compiling file: $filename") result <- frontend(contents) exitCode <- result.fold( code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), typedProg => val outDir = outputDir.getOrElse(Paths.get(filename).getParent) - IO.delay(Files.createDirectories(outDir)) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? + IO.delay( + Files.createDirectories(outDir) + ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? val outputFile = outDir.resolve(filename.stripSuffix(".wacc") + ".s") - writer.writeTo(backend(typedProg), outputFile) *> // TODO: I dont think we need IO here if we look at the implementation of writer - logAction(s"Compilation succeeded: $filename").as(0) + writer.writeTo( + backend(typedProg), + outputFile + ) *> // TODO: I dont think we need IO here if we look at the implementation of writer + logAction(s"Compilation succeeded: $filename").as(0) ) } yield exitCode // TODO: this is sequential, thus should be what occurs when --greedy is passed in val compileCommand: Opts[IO[ExitCode]] = - (filesOpt, logOpt, outputOpt).mapN{ - (files, log, outDir) => - files - .traverse{ file => - compile( - file.toAbsolutePath.toString, - outDir, - log) - }.map { - exitCodes => - if (exitCodes.exists(_ != 0)) ExitCode.Error // TODO- it should be the first one to exit when parallelised :) - else ExitCode.Success - } + (filesOpt, logOpt, outputOpt).mapN { (files, log, outDir) => + files + .traverse { file => + compile(file.toAbsolutePath.toString, outDir, log) + } + .map { exitCodes => + if (exitCodes.exists(_ != 0)) + ExitCode.Error // TODO- it should be the first one to exit when parallelised :) + else ExitCode.Success + } } @@ -127,4 +134,4 @@ object Main def main: Opts[IO[ExitCode]] = compileCommand -} \ No newline at end of file +} From abb43b560d439fd5f610fca6db1d37090698783a Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 2 Mar 2025 03:26:28 +0000 Subject: [PATCH 11/32] refactor: improve resource safety and structure of writer --- src/main/wacc/backend/writer.scala | 35 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/wacc/backend/writer.scala b/src/main/wacc/backend/writer.scala index 8789835..cae7110 100644 --- a/src/main/wacc/backend/writer.scala +++ b/src/main/wacc/backend/writer.scala @@ -13,15 +13,30 @@ import java.nio.file.Path object writer { import assemblyIR._ + // TODO: Judging from documentation it seems as though IO.blocking is the correct choice + // But needs checking + + /** Creates a resource safe BufferedWriter */ + private def bufferedWriter(outputPath: Path): Resource[IO, BufferedWriter] = + Resource.make { + IO.blocking(new BufferedWriter(new FileWriter(outputPath.toFile, StandardCharsets.UTF_8))) + } { writer => + IO.blocking(writer.close()) + .handleErrorWith(_ => IO.unit) // TODO: ensures writer is closed even if an error occurs + } + + /** Write line safely into a BufferedWriter */ + private def writeLines(writer: BufferedWriter, lines: Chain[AsmLine]): IO[Unit] = + IO.blocking { + lines.iterator.foreach { line => + writer.write(line.toString) + writer.newLine() + } + } + + /** Main function to write assembly to a file */ def writeTo(asmList: Chain[AsmLine], outputPath: Path)(using logger: Logger[IO]): IO[Unit] = - Resource - .fromAutoCloseable { - IO(BufferedWriter(FileWriter(outputPath.toFile, StandardCharsets.UTF_8))) - } - .use { writer => - IO { - asmList.iterator.foreach(line => writer.write(line.toString + "\n")) - writer.flush() // TODO: NECESSARY OR NOT? - } *> logger.info(s"Success: ${outputPath.toAbsolutePath}") - } + bufferedWriter(outputPath).use { writer => + writeLines(writer, asmList) *> logger.info(s"Success: ${outputPath.toAbsolutePath}") + } } From f66f1ab3ac13864addc7fddbc3b6f165adee758e Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 2 Mar 2025 03:48:37 +0000 Subject: [PATCH 12/32] refactor: compile function split up into smaller functions --- src/main/wacc/Main.scala | 81 ++++++++++++++++++++++-------- src/main/wacc/backend/writer.scala | 4 +- src/test/wacc/examples.scala | 3 +- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 7afdd4f..a47c34c 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -4,7 +4,7 @@ import scala.collection.mutable import cats.data.{Chain, NonEmptyList} import parsley.{Failure, Success} -import java.nio.file.{Files, Path, Paths} +import java.nio.file.{Files, Path} import cats.syntax.all._ import cats.effect.IO @@ -83,38 +83,75 @@ def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = // TODO: filename being String seems unnatural due to Path refactor // TODO: this function is doing too much should refactor -def compile(filename: String, outputDir: Option[Path], log: Boolean): IO[Int] = +// def compile(filename: String, outputDir: Option[Path], log: Boolean): IO[Int] = +// val logAction: String => IO[Unit] = +// if (log) logger.info(_) +// else (_ => IO.unit) +// for { +// contents <- IO.delay( +// os.read(os.Path(filename)) +// ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? +// _ <- logAction(s"Compiling file: $filename") +// result <- frontend(contents) +// exitCode <- result.fold( +// code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), +// typedProg => +// val outDir = outputDir.getOrElse(Paths.get(filename).getParent) +// IO.delay( +// Files.createDirectories(outDir) +// ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? +// val outputFile = outDir.resolve(filename.stripSuffix(".wacc") + ".s") +// writer.writeTo( +// backend(typedProg), +// outputFile +// ) *> // TODO: I dont think we need IO here if we look at the implementation of writer +// logAction(s"Compilation succeeded: $filename").as(0) +// ) +// } yield exitCode + +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 + } + + // 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 <- IO.delay( - os.read(os.Path(filename)) - ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? - _ <- logAction(s"Compiling file: $filename") - result <- frontend(contents) - exitCode <- result.fold( - code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), - typedProg => - val outDir = outputDir.getOrElse(Paths.get(filename).getParent) - IO.delay( - Files.createDirectories(outDir) - ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? - val outputFile = outDir.resolve(filename.stripSuffix(".wacc") + ".s") - writer.writeTo( - backend(typedProg), - outputFile - ) *> // TODO: I dont think we need IO here if we look at the implementation of writer - logAction(s"Compilation succeeded: $filename").as(0) - ) + contents <- readSourceFile + _ <- logAction(s"Compiling file: ${filePath.toAbsolutePath}") + outDir <- ensureOutputDir(outputDir.getOrElse(filePath.getParent)) + exitCode <- processProgram(contents, outDir) } yield exitCode +} // TODO: this is sequential, thus should be what occurs when --greedy is passed in val compileCommand: Opts[IO[ExitCode]] = (filesOpt, logOpt, outputOpt).mapN { (files, log, outDir) => files .traverse { file => - compile(file.toAbsolutePath.toString, outDir, log) + compile(file.toAbsolutePath, outDir, log) } .map { exitCodes => if (exitCodes.exists(_ != 0)) diff --git a/src/main/wacc/backend/writer.scala b/src/main/wacc/backend/writer.scala index cae7110..a339f55 100644 --- a/src/main/wacc/backend/writer.scala +++ b/src/main/wacc/backend/writer.scala @@ -36,7 +36,7 @@ object writer { /** Main function to write assembly to a file */ def writeTo(asmList: Chain[AsmLine], outputPath: Path)(using logger: Logger[IO]): IO[Unit] = - bufferedWriter(outputPath).use { writer => - writeLines(writer, asmList) *> logger.info(s"Success: ${outputPath.toAbsolutePath}") + bufferedWriter(outputPath).use { + writeLines(_, asmList) } } diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index ca1fe9d..11093d6 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -6,6 +6,7 @@ import org.scalatest.matchers.should.Matchers._ import org.scalatest.freespec.AsyncFreeSpec import cats.effect.testing.scalatest.AsyncIOSpec import java.io.File +import java.nio.file.Path import sys.process._ import scala.io.Source import cats.effect.IO @@ -32,7 +33,7 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd s"$filename" - { "should be compiled with correct result" in { - compileWacc(filename, outputDir = None, log = false).map { result => + compileWacc(Path.of(filename), outputDir = None, log = false).map { result => expectedResult should contain(result) } } From 473189342b343d3009ce81a0405b14ed0d2fab8f Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 2 Mar 2025 03:49:21 +0000 Subject: [PATCH 13/32] refactor: remove commented out code in main.scala --- src/main/wacc/Main.scala | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index a47c34c..750a548 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -81,34 +81,6 @@ def frontend( def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = asmGenerator.generateAsm(typedProg) -// TODO: filename being String seems unnatural due to Path refactor -// TODO: this function is doing too much should refactor -// def compile(filename: String, outputDir: Option[Path], log: Boolean): IO[Int] = -// val logAction: String => IO[Unit] = -// if (log) logger.info(_) -// else (_ => IO.unit) -// for { -// contents <- IO.delay( -// os.read(os.Path(filename)) -// ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? -// _ <- logAction(s"Compiling file: $filename") -// result <- frontend(contents) -// exitCode <- result.fold( -// code => logger.error(s"Compilation failed for $filename\nExit code: $code").as(code), -// typedProg => -// val outDir = outputDir.getOrElse(Paths.get(filename).getParent) -// IO.delay( -// Files.createDirectories(outDir) -// ) // TODO: Is IO as a wrapper ok or do we require .delay - also, should it be .blocking? -// val outputFile = outDir.resolve(filename.stripSuffix(".wacc") + ".s") -// writer.writeTo( -// backend(typedProg), -// outputFile -// ) *> // TODO: I dont think we need IO here if we look at the implementation of writer -// logAction(s"Compilation succeeded: $filename").as(0) -// ) -// } yield exitCode - def compile(filePath: Path, outputDir: Option[Path], log: Boolean): IO[Int] = { val logAction: String => IO[Unit] = if (log) logger.info(_) From 19e7ce4c113a17a0b271f181beefb2bd3331cf84 Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 2 Mar 2025 06:20:19 +0000 Subject: [PATCH 14/32] fix: fix output flag not reading path passed in --- src/main/wacc/Main.scala | 53 +++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 750a548..c85e2db 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -18,20 +18,35 @@ import org.typelevel.log4cats.slf4j.Slf4jLogger import org.typelevel.log4cats.Logger import assemblyIR as asm +import java.nio.file.Paths -// TODO: IO correctness, --greedy, parallelisable, and probably splitting this file up +/* +TODO: + 1) IO correctness + 2) --greedy, + 3) parallelised compilation + 4) splitting the file up and nicer refactoring + 5) logging could be removed + 6) errors can be handled more gracefully probably + */ given Argument[Path] = Argument.from("path") { str => - val path = Path.of(str) - ( - Either.cond(Files.exists(path), path, s"File '${path.toAbsolutePath}' does not exist"), - Either.cond( - Files.isRegularFile(path), - path, - s"File '${path.toAbsolutePath}' must be a regular file" - ), - Either.cond(path.toString.endsWith(".wacc"), path, "File must have .wacc extension") - ).mapN((_, _, _) => path).toValidatedNel + val path = + if (str.startsWith("~")) Paths.get(System.getProperty("user.home"), str.drop(1)) // Expand ~ + else Paths.get(str).toAbsolutePath.normalize() // TODO: normalize or not? + + if (path.toString.endsWith(".wacc")) { + ( + Either.cond(Files.exists(path), path, s"File '${path.toAbsolutePath}' does not exist"), + Either.cond( + Files.isRegularFile(path), + path, + s"File '${path.toAbsolutePath}' must be a regular file" + ) + ).mapN((_, _) => path).toValidatedNel + } else { + Right(path).toValidatedNel + } } given logger: Logger[IO] = Slf4jLogger.getLogger[IO] @@ -93,6 +108,15 @@ def compile(filePath: Path, outputDir: Option[Path], log: Boolean): IO[Int] = { 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) @@ -123,7 +147,12 @@ val compileCommand: Opts[IO[ExitCode]] = (filesOpt, logOpt, outputOpt).mapN { (files, log, outDir) => files .traverse { file => - compile(file.toAbsolutePath, outDir, log) + 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.pure(ExitCode(-1)) + } } .map { exitCodes => if (exitCodes.exists(_ != 0)) From f24aecffa3b643f51927ce4ea00e264bb67c9d0d Mon Sep 17 00:00:00 2001 From: Jonny Date: Mon, 3 Mar 2025 02:10:18 +0000 Subject: [PATCH 15/32] fix: remove implicit val causing conflicts with parsing cli arguments --- src/main/wacc/Main.scala | 42 ++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index c85e2db..571103b 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -18,7 +18,7 @@ import org.typelevel.log4cats.slf4j.Slf4jLogger import org.typelevel.log4cats.Logger import assemblyIR as asm -import java.nio.file.Paths +import cats.data.ValidatedNel /* TODO: @@ -30,34 +30,30 @@ TODO: 6) errors can be handled more gracefully probably */ -given Argument[Path] = Argument.from("path") { str => - val path = - if (str.startsWith("~")) Paths.get(System.getProperty("user.home"), str.drop(1)) // Expand ~ - else Paths.get(str).toAbsolutePath.normalize() // TODO: normalize or not? - - if (path.toString.endsWith(".wacc")) { - ( - Either.cond(Files.exists(path), path, s"File '${path.toAbsolutePath}' does not exist"), - Either.cond( - Files.isRegularFile(path), - path, - s"File '${path.toAbsolutePath}' must be a regular file" - ) - ).mapN((_, _) => path).toValidatedNel - } else { - Right(path).toValidatedNel - } -} - given logger: Logger[IO] = Slf4jLogger.getLogger[IO] val logOpt: Opts[Boolean] = Opts.flag("log", "Enable logging for additional compilation details", short = "l").orFalse -val outputOpt: Opts[Option[Path]] = - Opts.option[Path]("output", "Specify path for output assembly file(s)").orNone +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") +val filesOpt: Opts[NonEmptyList[Path]] = + Opts.arguments[Path]("files").mapValidated { + _.traverse(validateFile) + } + +// TODO: Is intermediate String necessary +val outputOpt: Opts[Option[Path]] = + Opts + .option[Path]("output", metavar = "path", help = "Output directory for compiled files.") + .orNone def frontend( contents: String From 94ee489fafdf6286e4e499a021fd72be89096b0a Mon Sep 17 00:00:00 2001 From: Jonny Date: Mon, 3 Mar 2025 02:58:04 +0000 Subject: [PATCH 16/32] feat: greedy cli argument implemented, parallel compilation now by default, but no fail fast behaviour --- src/main/wacc/Main.scala | 70 ++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 571103b..33c4fef 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -23,11 +23,10 @@ import cats.data.ValidatedNel /* TODO: 1) IO correctness - 2) --greedy, - 3) parallelised compilation - 4) splitting the file up and nicer refactoring - 5) logging could be removed - 6) errors can be handled more gracefully probably + 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] @@ -49,12 +48,14 @@ val filesOpt: Opts[NonEmptyList[Path]] = _.traverse(validateFile) } -// TODO: Is intermediate String necessary 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]] = { @@ -138,27 +139,43 @@ def compile(filePath: Path, outputDir: Option[Path], log: Boolean): IO[Int] = { } yield exitCode } -// TODO: this is sequential, thus should be what occurs when --greedy is passed in -val compileCommand: Opts[IO[ExitCode]] = - (filesOpt, logOpt, outputOpt).mapN { (files, log, outDir) => - 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.pure(ExitCode(-1)) - } +// 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 // TODO- it should be the first one to exit when parallelised :) - else ExitCode.Success + } + .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 + } - } - -// TODO: add parallelisable option object Main extends CommandIOApp( name = "wacc", @@ -166,6 +183,9 @@ object Main version = "1.0" ) { def main: Opts[IO[ExitCode]] = - compileCommand + (greedyOpt, filesOpt, logOpt, outputOpt).mapN { (greedy, files, log, outDir) => + if (greedy) compileCommandSequential(files, log, outDir) + else compileCommandParallel(files, log, outDir) + } } From 96ba81e24aef2eadfd663fcea6adcea502c19a7b Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Sun, 9 Mar 2025 23:37:04 +0000 Subject: [PATCH 17/32] refactor: consistent error handling in Main.scala --- project.scala | 2 - src/main/wacc/Main.scala | 126 +++++++++++------------------ src/main/wacc/frontend/Error.scala | 23 +++++- src/main/wacc/frontend/ast.scala | 1 - 4 files changed, 69 insertions(+), 83 deletions(-) diff --git a/project.scala b/project.scala index d8b5880..8edf035 100644 --- a/project.scala +++ b/project.scala @@ -20,5 +20,3 @@ //> using options -Yexplicit-nulls -Wsafe-init -Xkind-projector:underscores // repositories for pre-release versions if needed -//> using repositories sonatype-s01:releases -//> using repositories sonatype-s01:snapshots diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 33c4fef..d964657 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -12,7 +12,6 @@ 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 @@ -29,6 +28,9 @@ TODO: 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] = @@ -51,49 +53,46 @@ val filesOpt: Opts[NonEmptyList[Path]] = 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 -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)) - +): Either[NonEmptyList[Error], microWacc.Program] = + parser.parse(contents) match { + case Failure(msg) => Left(NonEmptyList.one(Error.SyntaxError(msg))) 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)) + NonEmptyList.fromList(errors.result) match { + case Some(errors) => Left(errors) + case None => Right(typedProg) } } -} def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = asmGenerator.generateAsm(typedProg) -def compile(filePath: Path, outputDir: Option[Path], log: Boolean): IO[Int] = { +def compile( + filePath: Path, + outputDir: Option[Path], + log: Boolean +): IO[Int] = { val logAction: String => IO[Unit] = if (log) logger.info(_) else (_ => IO.unit) @@ -101,79 +100,49 @@ def compile(filePath: Path, outputDir: Option[Path], log: Boolean): IO[Int] = { 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) + frontend(contents) match { + case Left(errors) => + val code = errors.map(err => err.exitCode).toList.min + given errorContent: String = contents + 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 ${filePath.toAbsolutePath}:\n$errorMsg") + ) + } yield code case Right(typedProg) => val outputFile = outDir.resolve(filePath.getFileName.toString.stripSuffix(".wacc") + ".s") - writeOutputFile(typedProg, outputFile).as(0) + writeOutputFile(typedProg, outputFile).as(SUCCESS) } for { contents <- readSourceFile _ <- logAction(s"Compiling file: ${filePath.toAbsolutePath}") - outDir <- ensureOutputDir(outputDir.getOrElse(filePath.getParent)) - exitCode <- processProgram(contents, outDir) + exitCode <- processProgram(contents, outputDir.getOrElse(filePath.getParent)) } 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) - } - } + .parTraverse { file => compile(file.toAbsolutePath, outDir, log) } .map { exitCodes => - if (exitCodes.exists(_ != 0)) ExitCode.Error else ExitCode.Success + exitCodes.filter(_ != 0) match { + case Nil => ExitCode.Success + case errorCodes => ExitCode(errorCodes.min) + } } object Main @@ -183,9 +152,8 @@ object Main 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) + (filesOpt, logOpt, outputOpt).mapN { (files, log, outDir) => + compileCommandParallel(files, log, outDir) } } diff --git a/src/main/wacc/frontend/Error.scala b/src/main/wacc/frontend/Error.scala index b991421..e515494 100644 --- a/src/main/wacc/frontend/Error.scala +++ b/src/main/wacc/frontend/Error.scala @@ -3,6 +3,9 @@ package wacc import wacc.ast.Position import wacc.types._ +private val SYNTAX_ERROR = 100 +private val SEMANTIC_ERROR = 200 + /** Error types for semantic errors */ enum Error { @@ -14,6 +17,15 @@ enum Error { case SemanticError(pos: Position, msg: String) case TypeMismatch(pos: Position, expected: SemType, got: SemType, msg: String) case InternalError(pos: Position, msg: String) + + case SyntaxError(msg: String) +} + +extension (e: Error) { + def exitCode: Int = e match { + case Error.SyntaxError(_) => SYNTAX_ERROR + case _ => SEMANTIC_ERROR + } } /** Function to handle printing the details of a given semantic error @@ -25,7 +37,6 @@ enum Error { */ def formatError(error: Error)(using errorContent: String): String = { val sb = new StringBuilder() - sb.append("Semantic error:\n") /** Function to format the position of an error * @@ -55,6 +66,13 @@ def formatError(error: Error)(using errorContent: String): String = { ) } + error match { + case Error.SyntaxError(_) => + sb.append("Syntax error:\n") + case _ => + sb.append("Semantic error:\n") + } + error match { case Error.DuplicateDeclaration(ident) => formatPosition(ident.pos) @@ -87,6 +105,9 @@ def formatError(error: Error)(using errorContent: String): String = { formatPosition(pos) sb.append(s"Internal error: $msg") formatHighlight(pos, 1) + case Error.SyntaxError(msg) => + sb.append(msg) + sb.append("\n") } sb.toString() diff --git a/src/main/wacc/frontend/ast.scala b/src/main/wacc/frontend/ast.scala index ac0e585..9b14b13 100644 --- a/src/main/wacc/frontend/ast.scala +++ b/src/main/wacc/frontend/ast.scala @@ -4,7 +4,6 @@ import parsley.Parsley import parsley.generic.ErrorBridge import parsley.ap._ import parsley.position._ -import parsley.syntax.zipped._ import cats.data.NonEmptyList object ast { From 0d8be53ae45c1db87cc18919a3a3cfa95dd57a2e Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Wed, 12 Mar 2025 23:34:39 +0000 Subject: [PATCH 18/32] fix: set output to `.` for labts compiler --- compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compile b/compile index 5505e42..444f591 100755 --- a/compile +++ b/compile @@ -4,6 +4,6 @@ # but do *not* change its name. # feel free to adjust to suit the specific internal flags of your compiler -./wacc-compiler "$@" +./wacc-compiler --output . "$@" exit $? From e881b736f874fe2f67f8356c039e7d1588f46338 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 08:18:44 +0000 Subject: [PATCH 19/32] feat: imports parser --- src/main/wacc/Main.scala | 2 +- src/main/wacc/frontend/ast.scala | 12 ++++++++++++ src/main/wacc/frontend/parser.scala | 24 +++++++++++++++++++----- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index d964657..3d16c33 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -72,7 +72,7 @@ def frontend( ): Either[NonEmptyList[Error], microWacc.Program] = parser.parse(contents) match { case Failure(msg) => Left(NonEmptyList.one(Error.SyntaxError(msg))) - case Success(prog) => + case Success(ast.PartialProgram(_, prog)) => given errors: mutable.Builder[Error, List[Error]] = List.newBuilder val (names, funcs) = renamer.rename(prog) diff --git a/src/main/wacc/frontend/ast.scala b/src/main/wacc/frontend/ast.scala index 9b14b13..e331e2d 100644 --- a/src/main/wacc/frontend/ast.scala +++ b/src/main/wacc/frontend/ast.scala @@ -131,6 +131,18 @@ object ast { /* ============================ PROGRAM STRUCTURE ============================ */ + case class ImportedFunc(sourceName: Ident, importName: Ident)(val pos: Position) + object ImportedFunc extends ParserBridgePos2[Ident, Option[Ident], ImportedFunc] { + def apply(a: Ident, b: Option[Ident])(pos: Position): ImportedFunc = + new ImportedFunc(a, b.getOrElse(a))(pos) + } + + case class Import(source: StrLiter, funcs: NonEmptyList[ImportedFunc])(val pos: Position) + object Import extends ParserBridgePos2[StrLiter, NonEmptyList[ImportedFunc], Import] + + case class PartialProgram(imports: List[Import], self: Program)(val pos: Position) + object PartialProgram extends ParserBridgePos2[List[Import], Program, PartialProgram] + case class Program(funcs: List[FuncDecl], main: NonEmptyList[Stmt])(val pos: Position) object Program extends ParserBridgePos2[List[FuncDecl], NonEmptyList[Stmt], Program] diff --git a/src/main/wacc/frontend/parser.scala b/src/main/wacc/frontend/parser.scala index e798284..9f3fa40 100644 --- a/src/main/wacc/frontend/parser.scala +++ b/src/main/wacc/frontend/parser.scala @@ -3,12 +3,12 @@ package wacc import parsley.Result import parsley.Parsley import parsley.Parsley.{atomic, many, notFollowedBy, pure, unit} -import parsley.combinator.{countSome, sepBy} +import parsley.combinator.{countSome, sepBy, option} import parsley.expr.{precedence, SOps, InfixL, InfixN, InfixR, Prefix, Atoms} import parsley.errors.combinator._ import parsley.errors.patterns.VerifiedErrors import parsley.syntax.zipped._ -import parsley.cats.combinator.{some} +import parsley.cats.combinator.{some, sepBy1} import cats.data.NonEmptyList import parsley.errors.DefaultErrorBuilder import parsley.errors.ErrorBuilder @@ -52,8 +52,8 @@ object parser { implicit val builder: ErrorBuilder[String] = new DefaultErrorBuilder with LexToken { def tokens = errTokens } - def parse(input: String): Result[String, Program] = parser.parse(input) - private val parser = lexer.fully(``) + def parse(input: String): Result[String, PartialProgram] = parser.parse(input) + private val parser = lexer.fully(``) // Expressions private lazy val ``: Parsley[Expr] = precedence { @@ -87,11 +87,12 @@ object parser { IntLiter(integer).label("integer literal"), BoolLiter(("true" as true) | ("false" as false)).label("boolean literal"), CharLiter(charLit).label("character literal"), - StrLiter(stringLit).label("string literal"), + ``.label("string literal"), PairLiter from "null", ``, Parens("(" ~> `` <~ ")") ) + private val `` = StrLiter(stringLit) private val `` = Ident(ident) | some("*" | "&").verifiedExplain("pointer operators are not allowed") private lazy val `` = @@ -127,6 +128,19 @@ object parser { invalid syntax check, this only happens at most once per program so this is not a major concern. */ + private lazy val `` = PartialProgram( + many(``), + `` + ) + private lazy val `` = Import( + "import" ~> ``, + "(" ~> sepBy1(``, ",") <~ ")" + ) + private lazy val `` = ``.label("import file name") + private lazy val `` = ImportedFunc( + ``.label("imported function name"), + option("as" ~> ``).label("imported function alias") + ) private lazy val `` = Program( "begin" ~> ( many( From f11fb9f881c99585564928834458427daf2966b0 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 09:43:29 +0000 Subject: [PATCH 20/32] test: integration tests for imports --- .gitignore | 1 - .../examples/invalid/semantics/badWacc.wacc | 10 ++++ .../semantics/imports/importBadFile.wacc | 6 +++ .../semantics/imports/importBadFunc.wacc | 6 +++ .../semantics/imports/importBadSem.wacc | 10 ++++ .../semantics/imports/importBadSem2.wacc | 6 +++ .../invalid/semantics/imports/inderect.wacc | 6 +++ .../examples/invalid/syntax/badWacc.wacc | 6 +++ .../invalid/syntax/imports/emptyImport.wacc | 8 ++++ .../invalid/syntax/imports/emptyImport2.wacc | 5 ++ .../syntax/imports/importBadSyntax.wacc | 10 ++++ .../syntax/imports/importBadSyntax2.wacc | 6 +++ .../syntax/imports/importNoParens.wacc | 1 + .../invalid/syntax/imports/importSemis.wacc | 9 ++++ .../invalid/syntax/imports/importStar.wacc | 5 ++ .../invalid/syntax/imports/importStar2.wacc | 5 ++ extension/examples/valid/.gitignore | 7 +++ extension/examples/valid/imports/alias.wacc | 22 +++++++++ extension/examples/valid/imports/basic.wacc | 20 ++++++++ .../examples/valid/imports/manyMains.wacc | 34 ++++++++++++++ .../examples/valid/imports/mutliFunc.wacc | 27 +++++++++++ extension/examples/valid/sum.wacc | 27 +++++++++++ src/test/wacc/examples.scala | 46 ++++++++++++++----- 23 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 extension/examples/invalid/semantics/badWacc.wacc create mode 100644 extension/examples/invalid/semantics/imports/importBadFile.wacc create mode 100644 extension/examples/invalid/semantics/imports/importBadFunc.wacc create mode 100644 extension/examples/invalid/semantics/imports/importBadSem.wacc create mode 100644 extension/examples/invalid/semantics/imports/importBadSem2.wacc create mode 100644 extension/examples/invalid/semantics/imports/inderect.wacc create mode 100644 extension/examples/invalid/syntax/badWacc.wacc create mode 100644 extension/examples/invalid/syntax/imports/emptyImport.wacc create mode 100644 extension/examples/invalid/syntax/imports/emptyImport2.wacc create mode 100644 extension/examples/invalid/syntax/imports/importBadSyntax.wacc create mode 100644 extension/examples/invalid/syntax/imports/importBadSyntax2.wacc create mode 100644 extension/examples/invalid/syntax/imports/importNoParens.wacc create mode 100644 extension/examples/invalid/syntax/imports/importSemis.wacc create mode 100644 extension/examples/invalid/syntax/imports/importStar.wacc create mode 100644 extension/examples/invalid/syntax/imports/importStar2.wacc create mode 100644 extension/examples/valid/.gitignore create mode 100644 extension/examples/valid/imports/alias.wacc create mode 100644 extension/examples/valid/imports/basic.wacc create mode 100644 extension/examples/valid/imports/manyMains.wacc create mode 100644 extension/examples/valid/imports/mutliFunc.wacc create mode 100644 extension/examples/valid/sum.wacc diff --git a/.gitignore b/.gitignore index 03801cc..dab1691 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ .vscode/ wacc-examples/ .idea/ - diff --git a/extension/examples/invalid/semantics/badWacc.wacc b/extension/examples/invalid/semantics/badWacc.wacc new file mode 100644 index 0000000..a334d57 --- /dev/null +++ b/extension/examples/invalid/semantics/badWacc.wacc @@ -0,0 +1,10 @@ +begin + int main() is + int a = 5 ; + string b = "Hello" ; + return a + b + end + + int result = call main() ; + exit result +end diff --git a/extension/examples/invalid/semantics/imports/importBadFile.wacc b/extension/examples/invalid/semantics/imports/importBadFile.wacc new file mode 100644 index 0000000..b116ccd --- /dev/null +++ b/extension/examples/invalid/semantics/imports/importBadFile.wacc @@ -0,0 +1,6 @@ +import "./doesNotExist.wacc" (main) + +begin + int result = call main() ; + exit result +end diff --git a/extension/examples/invalid/semantics/imports/importBadFunc.wacc b/extension/examples/invalid/semantics/imports/importBadFunc.wacc new file mode 100644 index 0000000..bf3c9a0 --- /dev/null +++ b/extension/examples/invalid/semantics/imports/importBadFunc.wacc @@ -0,0 +1,6 @@ +import "../../../valid/sum.wacc" (mult) + +begin + int result = call mult(3, 2) ; + exit result +end diff --git a/extension/examples/invalid/semantics/imports/importBadSem.wacc b/extension/examples/invalid/semantics/imports/importBadSem.wacc new file mode 100644 index 0000000..d20e3a6 --- /dev/null +++ b/extension/examples/invalid/semantics/imports/importBadSem.wacc @@ -0,0 +1,10 @@ +import "../badWacc.wacc" (main) + +begin + int sum(int a, int b) is + return a + b + end + + int result = call main() ; + exit result +end diff --git a/extension/examples/invalid/semantics/imports/importBadSem2.wacc b/extension/examples/invalid/semantics/imports/importBadSem2.wacc new file mode 100644 index 0000000..4bd330e --- /dev/null +++ b/extension/examples/invalid/semantics/imports/importBadSem2.wacc @@ -0,0 +1,6 @@ +import "./importBadSem.wacc" (sum) + +begin + int result = call sum(1, 2) ; + exit result +end diff --git a/extension/examples/invalid/semantics/imports/inderect.wacc b/extension/examples/invalid/semantics/imports/inderect.wacc new file mode 100644 index 0000000..120f9ba --- /dev/null +++ b/extension/examples/invalid/semantics/imports/inderect.wacc @@ -0,0 +1,6 @@ +import "../../../valid/imports/basic.wacc" (sum) + +begin + int result = call sum(3, 2) ; + exit result +end diff --git a/extension/examples/invalid/syntax/badWacc.wacc b/extension/examples/invalid/syntax/badWacc.wacc new file mode 100644 index 0000000..a375309 --- /dev/null +++ b/extension/examples/invalid/syntax/badWacc.wacc @@ -0,0 +1,6 @@ +int main() is + println "Hello World!" ; + return 0 +end + +skip diff --git a/extension/examples/invalid/syntax/imports/emptyImport.wacc b/extension/examples/invalid/syntax/imports/emptyImport.wacc new file mode 100644 index 0000000..ec9dbd0 --- /dev/null +++ b/extension/examples/invalid/syntax/imports/emptyImport.wacc @@ -0,0 +1,8 @@ +import "../../../valid/sum.wacc" sum, main + +begin + int result1 = call sum(5, 10) ; + int result2 = call main() ; + println result1 ; + println result2 +end diff --git a/extension/examples/invalid/syntax/imports/emptyImport2.wacc b/extension/examples/invalid/syntax/imports/emptyImport2.wacc new file mode 100644 index 0000000..99d38b9 --- /dev/null +++ b/extension/examples/invalid/syntax/imports/emptyImport2.wacc @@ -0,0 +1,5 @@ +import "../../../valid/sum.wacc" () + +begin + exit 0 +end diff --git a/extension/examples/invalid/syntax/imports/importBadSyntax.wacc b/extension/examples/invalid/syntax/imports/importBadSyntax.wacc new file mode 100644 index 0000000..d20e3a6 --- /dev/null +++ b/extension/examples/invalid/syntax/imports/importBadSyntax.wacc @@ -0,0 +1,10 @@ +import "../badWacc.wacc" (main) + +begin + int sum(int a, int b) is + return a + b + end + + int result = call main() ; + exit result +end diff --git a/extension/examples/invalid/syntax/imports/importBadSyntax2.wacc b/extension/examples/invalid/syntax/imports/importBadSyntax2.wacc new file mode 100644 index 0000000..0e0e0e1 --- /dev/null +++ b/extension/examples/invalid/syntax/imports/importBadSyntax2.wacc @@ -0,0 +1,6 @@ +import "./importBadSyntax.wacc" (sum) + +begin + int result = call sum(1, 2) ; + exit result +end diff --git a/extension/examples/invalid/syntax/imports/importNoParens.wacc b/extension/examples/invalid/syntax/imports/importNoParens.wacc new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/extension/examples/invalid/syntax/imports/importNoParens.wacc @@ -0,0 +1 @@ + diff --git a/extension/examples/invalid/syntax/imports/importSemis.wacc b/extension/examples/invalid/syntax/imports/importSemis.wacc new file mode 100644 index 0000000..f127844 --- /dev/null +++ b/extension/examples/invalid/syntax/imports/importSemis.wacc @@ -0,0 +1,9 @@ +import "../../../valid/sum.wacc" (sum) ; +import "../../../valid/sum.wacc" (main) ; + +begin + int result1 = call sum(5, 10) ; + int result2 = call main() ; + println result1 ; + println result2 +end diff --git a/extension/examples/invalid/syntax/imports/importStar.wacc b/extension/examples/invalid/syntax/imports/importStar.wacc new file mode 100644 index 0000000..e027caa --- /dev/null +++ b/extension/examples/invalid/syntax/imports/importStar.wacc @@ -0,0 +1,5 @@ +import "../../../valid/sum.wacc" * + +begin + exit 0 +end diff --git a/extension/examples/invalid/syntax/imports/importStar2.wacc b/extension/examples/invalid/syntax/imports/importStar2.wacc new file mode 100644 index 0000000..bae08ef --- /dev/null +++ b/extension/examples/invalid/syntax/imports/importStar2.wacc @@ -0,0 +1,5 @@ +import "../../../valid/sum.wacc" (*) + +begin + exit 0 +end diff --git a/extension/examples/valid/.gitignore b/extension/examples/valid/.gitignore new file mode 100644 index 0000000..ed87167 --- /dev/null +++ b/extension/examples/valid/.gitignore @@ -0,0 +1,7 @@ +* + +!imports/ +imports/* + +!.gitignore +!*.wacc diff --git a/extension/examples/valid/imports/alias.wacc b/extension/examples/valid/imports/alias.wacc new file mode 100644 index 0000000..91496a1 --- /dev/null +++ b/extension/examples/valid/imports/alias.wacc @@ -0,0 +1,22 @@ +# import main from ../sum.wacc and ./basic.wacc + +# Output: +# 15 +# 0 +# -33 +# + +# Exit: +# 0 + +# Program: + +import "../sum.wacc" (main as sumMain) +import "./basic.wacc" (main) + +begin + int result1 = call sumMain() ; + int result2 = call main() ; + println result1 ; + println result2 +end diff --git a/extension/examples/valid/imports/basic.wacc b/extension/examples/valid/imports/basic.wacc new file mode 100644 index 0000000..aae959b --- /dev/null +++ b/extension/examples/valid/imports/basic.wacc @@ -0,0 +1,20 @@ +# import sum from ../sum.wacc + +# Output: +# -33 + +# Exit: +# 0 + +# Program: + +import "../sum.wacc" (sum) + +begin + int main() is + int result = call sum(-10, -23) ; + return result + end + int result = call main() ; + println result +end diff --git a/extension/examples/valid/imports/manyMains.wacc b/extension/examples/valid/imports/manyMains.wacc new file mode 100644 index 0000000..d52997f --- /dev/null +++ b/extension/examples/valid/imports/manyMains.wacc @@ -0,0 +1,34 @@ +# import all the mains + +# Output: +# 15 +# 0 +# -33 +# 0 +# -33 +# 0 +# + +# Exit: +# 99 + +# Program: + +import "../sum.wacc" (main as sumMain) +import "./basic.wacc" (main as basicMain) +import "./multiFunc.wacc" (main as multiFuncMain) + +begin + int main() is + int result1 = call sumMain() ; + int result2 = call basicMain() ; + int result3 = call multiFuncMain() ; + println result1 ; + println result2 ; + println result3 ; + return 99 + end + + int result = call main() ; + exit result +end diff --git a/extension/examples/valid/imports/mutliFunc.wacc b/extension/examples/valid/imports/mutliFunc.wacc new file mode 100644 index 0000000..22d6e4d --- /dev/null +++ b/extension/examples/valid/imports/mutliFunc.wacc @@ -0,0 +1,27 @@ +# import sum, main from ../sum.wacc + +# Output: +# 15 +# -33 +# 0 +# 0 +# + +# Exit: +# 0 + +# Program: + +import "../sum.wacc" (sum, main as sumMain) + +begin + int main() is + int result = call sum(-10, -23) ; + println result ; + return 0 + end + int result1 = call sumMain() ; + int result2 = call main() ; + println result1 ; + println result2 +end diff --git a/extension/examples/valid/sum.wacc b/extension/examples/valid/sum.wacc new file mode 100644 index 0000000..dc62e24 --- /dev/null +++ b/extension/examples/valid/sum.wacc @@ -0,0 +1,27 @@ +# simple sum program + +# Output: +# 15 +# + +# Exit: +# 0 + +# Program: + +begin + int sum(int a, int b) is + return a + b + end + + int main() is + int a = 5 ; + int b = 10 ; + int result = call sum(a, b) ; + println result ; + return 0 + end + + int result = call main() ; + exit result +end diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index 11093d6..13a3352 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -26,6 +26,15 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd } ++ allWaccFiles("wacc-examples/invalid/whack").map { p => (p.toString, List(100, 200)) + } ++ + allWaccFiles("extension/examples/valid").map { p => + (p.toString, List(0)) + } ++ + allWaccFiles("extension/examples/invalid/syntax").map { p => + (p.toString, List(100)) + } ++ + allWaccFiles("extension/examples/invalid/semantics").map { p => + (p.toString, List(200)) } forEvery(files) { (filename, expectedResult) => @@ -33,18 +42,21 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd s"$filename" - { "should be compiled with correct result" in { - compileWacc(Path.of(filename), outputDir = None, log = false).map { result => - expectedResult should contain(result) - } + if (fileIsPendingFrontend(filename)) + IO.pure(pending) + else + compileWacc(Path.of(filename), outputDir = None, log = false).map { result => + expectedResult should contain(result) + } } if (expectedResult == List(0)) { "should run with correct result" in { if (fileIsDisallowedBackend(filename)) - IO.pure( - succeed - ) // TODO: remove when advanced tests removed. not sure how to "pending" this otherwise - else { + IO.pure(succeed) + else if (fileIsPendingBackend(filename)) + IO.pure(pending) + else for { contents <- IO(Source.fromFile(File(filename)).getLines.toList) inputLine = extractInput(contents) @@ -75,7 +87,6 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd exitCode shouldBe expectedExit normalizeOutput(stdout.toString) shouldBe expectedOutput } - } } } } @@ -85,10 +96,21 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd val d = java.io.File(dir) os.walk(os.Path(d.getAbsolutePath)).filter(_.ext == "wacc") - // TODO: eventually remove this I think - def fileIsDisallowedBackend(filename: String): Boolean = - Seq( - "^.*wacc-examples/valid/advanced.*$" + private def fileIsDisallowedBackend(filename: String): Boolean = + filename.matches("^.*wacc-examples/valid/advanced.*$") + + private def fileIsPendingFrontend(filename: String): Boolean = + List( + "^.*extension/examples/invalid/syntax/imports/importBadSyntax.*$", + "^.*extension/examples/invalid/semantics/imports.*$", + "^.*extension/examples/valid/imports.*$" + ).exists(filename.matches) + + private def fileIsPendingBackend(filename: String): Boolean = + List( + "^.*extension/examples/invalid/syntax/imports.*$", + "^.*extension/examples/invalid/semantics/imports.*$", + "^.*extension/examples/valid/imports.*$" ).exists(filename.matches) private def extractInput(contents: List[String]): String = From 3fff9d3825b64ea8c663ac8b2363b6de47abb012 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 12:40:57 +0000 Subject: [PATCH 21/32] feat: file parser bridges --- src/main/wacc/frontend/ast.scala | 95 ++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/src/main/wacc/frontend/ast.scala b/src/main/wacc/frontend/ast.scala index e331e2d..4a397a9 100644 --- a/src/main/wacc/frontend/ast.scala +++ b/src/main/wacc/frontend/ast.scala @@ -1,5 +1,6 @@ package wacc +import java.io.File import parsley.Parsley import parsley.generic.ErrorBridge import parsley.ap._ @@ -22,26 +23,39 @@ object ast { /* ============================ ATOMIC EXPRESSIONS ============================ */ case class IntLiter(v: Int)(val pos: Position) extends Expr6 - object IntLiter extends ParserBridgePos1[Int, IntLiter] + object IntLiter extends ParserBridgePos1Atom[Int, IntLiter] case class BoolLiter(v: Boolean)(val pos: Position) extends Expr6 - object BoolLiter extends ParserBridgePos1[Boolean, BoolLiter] + object BoolLiter extends ParserBridgePos1Atom[Boolean, BoolLiter] case class CharLiter(v: Char)(val pos: Position) extends Expr6 - object CharLiter extends ParserBridgePos1[Char, CharLiter] + object CharLiter extends ParserBridgePos1Atom[Char, CharLiter] case class StrLiter(v: String)(val pos: Position) extends Expr6 - object StrLiter extends ParserBridgePos1[String, StrLiter] + object StrLiter extends ParserBridgePos1Atom[String, StrLiter] case class PairLiter()(val pos: Position) extends Expr6 object PairLiter extends ParserBridgePos0[PairLiter] case class Ident(v: String, var uid: Int = -1)(val pos: Position) extends Expr6 with LValue - object Ident extends ParserBridgePos1[String, Ident] { + object Ident extends ParserBridgePos1Atom[String, Ident] { def apply(v: String)(pos: Position): Ident = new Ident(v)(pos) } case class ArrayElem(name: Ident, indices: NonEmptyList[Expr])(val pos: Position) extends Expr6 with LValue - object ArrayElem extends ParserBridgePos1[NonEmptyList[Expr], Ident => ArrayElem] { - def apply(a: NonEmptyList[Expr])(pos: Position): Ident => ArrayElem = - name => ArrayElem(name, a)(pos) + object ArrayElem extends ParserBridgePos2Chain[NonEmptyList[Expr], Ident, ArrayElem] { + def apply(indices: NonEmptyList[Expr], name: Ident)(pos: Position): ArrayElem = + new ArrayElem(name, indices)(pos) } + // object ArrayElem extends ParserBridgePos1[NonEmptyList[Expr], (File => Ident) => ArrayElem] { + // def apply(a: NonEmptyList[Expr])(pos: Position): (File => Ident) => ArrayElem = + // name => ArrayElem(name(pos.file), a)(pos) + // } + // object ArrayElem extends ParserSingletonBridgePos[(File => NonEmptyList[Expr]) => (File => Ident) => File => ArrayElem] { + // // def apply(indices: NonEmptyList[Expr]): (File => Ident) => File => ArrayElem = + // // name => file => new ArrayElem(name(file), ) + // def apply(indices: Parsley[File => NonEmptyList[Expr]]): Parsley[(File => Ident) => File => ArrayElem] = + // // error(ap1(pos.map(con),)) + + // override final def con(pos: (Int, Int)): (File => NonEmptyList[Expr]) => => C = + // (a, b) => file => this.apply(a(file), b(file))(Position(pos._1, pos._2, file)) + // } case class Parens(expr: Expr)(val pos: Position) extends Expr6 object Parens extends ParserBridgePos1[Expr, Parens] @@ -119,8 +133,8 @@ object ast { case class ArrayType(elemType: Type, dimensions: Int)(val pos: Position) extends Type with PairElemType - object ArrayType extends ParserBridgePos1[Int, Type => ArrayType] { - def apply(a: Int)(pos: Position): Type => ArrayType = elemType => ArrayType(elemType, a)(pos) + object ArrayType extends ParserBridgePos2Chain[Int, Type, ArrayType] { + def apply(dimensions: Int, elemType: Type)(pos: Position): ArrayType = ArrayType(elemType, dimensions)(pos) } case class PairType(fst: PairElemType, snd: PairElemType)(val pos: Position) extends Type object PairType extends ParserBridgePos2[PairElemType, PairElemType, PairType] @@ -155,15 +169,12 @@ object ast { body: NonEmptyList[Stmt] )(val pos: Position) object FuncDecl - extends ParserBridgePos2[ - List[Param], - NonEmptyList[Stmt], - ((Type, Ident)) => FuncDecl + extends ParserBridgePos2Chain[ + (List[Param], NonEmptyList[Stmt]), + ((Type, Ident)), FuncDecl ] { - def apply(params: List[Param], body: NonEmptyList[Stmt])( - pos: Position - ): ((Type, Ident)) => FuncDecl = - (returnType, name) => FuncDecl(returnType, name, params, body)(pos) + def apply(paramsBody: (List[Param], NonEmptyList[Stmt]), retTyName: (Type, Ident))(pos: Position): FuncDecl = + new FuncDecl(retTyName._1, retTyName._2, paramsBody._1, paramsBody._2)(pos) } case class Param(paramType: Type, name: Ident)(val pos: Position) @@ -219,7 +230,7 @@ object ast { /* ============================ PARSER BRIDGES ============================ */ - case class Position(line: Int, column: Int) + case class Position(line: Int, column: Int, file: File) trait ParserSingletonBridgePos[+A] extends ErrorBridge { protected def con(pos: (Int, Int)): A @@ -227,38 +238,54 @@ object ast { final def <#(op: Parsley[?]): Parsley[A] = this from op } - trait ParserBridgePos0[+A] extends ParserSingletonBridgePos[A] { + trait ParserBridgePos0[+A] extends ParserSingletonBridgePos[File => A] { def apply()(pos: Position): A - override final def con(pos: (Int, Int)): A = - apply()(Position(pos._1, pos._2)) + override final def con(pos: (Int, Int)): File => A = + file => apply()(Position(pos._1, pos._2, file)) } - trait ParserBridgePos1[-A, +B] extends ParserSingletonBridgePos[A => B] { + trait ParserBridgePos1Atom[-A, +B] extends ParserSingletonBridgePos[A => File => B] { def apply(a: A)(pos: Position): B - def apply(a: Parsley[A]): Parsley[B] = error(ap1(pos.map(con), a)) + def apply(a: Parsley[A]): Parsley[File => B] = error(ap1(pos.map(con), a)) - override final def con(pos: (Int, Int)): A => B = - this.apply(_)(Position(pos._1, pos._2)) + override final def con(pos: (Int, Int)): A => File => B = + a => file => this.apply(a)(Position(pos._1, pos._2, file)) } - trait ParserBridgePos2[-A, -B, +C] extends ParserSingletonBridgePos[(A, B) => C] { + trait ParserBridgePos1[-A, +B] extends ParserSingletonBridgePos[(File => A) => File => B] { + def apply(a: A)(pos: Position): B + def apply(a: Parsley[File => A]): Parsley[File => B] = error(ap1(pos.map(con), a)) + + override final def con(pos: (Int, Int)): (File => A) => File => B = + a => file => this.apply(a(file))(Position(pos._1, pos._2, file)) + } + + trait ParserBridgePos2Chain[-A, -B, +C] extends ParserSingletonBridgePos[(File => A) => (File => B) => File => C] { def apply(a: A, b: B)(pos: Position): C - def apply(a: Parsley[A], b: => Parsley[B]): Parsley[C] = error( + def apply(a: Parsley[File =>A]): Parsley[(File => B) => File => C] = error(ap1(pos.map(con), a)) + + override final def con(pos: (Int, Int)): (File => A) => (File => B) => File => C = + a => b => file => this.apply(a(file), b(file))(Position(pos._1, pos._2, file)) + } + + trait ParserBridgePos2[-A, -B, +C] extends ParserSingletonBridgePos[(File => A, File => B) => File => C] { + def apply(a: A, b: B)(pos: Position): C + def apply(a: Parsley[File => A], b: => Parsley[File => B]): Parsley[File => C] = error( ap2(pos.map(con), a, b) ) - override final def con(pos: (Int, Int)): (A, B) => C = - apply(_, _)(Position(pos._1, pos._2)) + override final def con(pos: (Int, Int)): (File => A, File => B) => File => C = + (a, b) => file => this.apply(a(file), b(file))(Position(pos._1, pos._2, file)) } - trait ParserBridgePos3[-A, -B, -C, +D] extends ParserSingletonBridgePos[(A, B, C) => D] { + trait ParserBridgePos3[-A, -B, -C, +D] extends ParserSingletonBridgePos[(File => A, File => B, File => C) => File => D] { def apply(a: A, b: B, c: C)(pos: Position): D - def apply(a: Parsley[A], b: => Parsley[B], c: => Parsley[C]): Parsley[D] = error( + def apply(a: Parsley[File => A], b: => Parsley[File => B], c: => Parsley[File => C]): Parsley[File => D] = error( ap3(pos.map(con), a, b, c) ) - override final def con(pos: (Int, Int)): (A, B, C) => D = - apply(_, _, _)(Position(pos._1, pos._2)) + override final def con(pos: (Int, Int)): (File => A, File => B, File => C) => File => D = + (a, b, c) => file => apply(a(file), b(file), c(file))(Position(pos._1, pos._2, file)) } } From 5141a2369f1cda510093998ba61aeef685fcd3f1 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 13:26:35 +0000 Subject: [PATCH 22/32] fix: convert parser to use FParsley --- src/main/wacc/Main.scala | 12 ++-- src/main/wacc/frontend/parser.scala | 85 ++++++++++++++++++----------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 3d16c33..68688bc 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -18,6 +18,7 @@ import org.typelevel.log4cats.Logger import assemblyIR as asm import cats.data.ValidatedNel +import java.io.File /* TODO: @@ -68,11 +69,12 @@ val outputOpt: Opts[Option[Path]] = .orNone def frontend( - contents: String + contents: String, file: File ): Either[NonEmptyList[Error], microWacc.Program] = parser.parse(contents) match { case Failure(msg) => Left(NonEmptyList.one(Error.SyntaxError(msg))) - case Success(ast.PartialProgram(_, prog)) => + case Success(fn) => + val ast.PartialProgram(_, prog) = fn(file) given errors: mutable.Builder[Error, List[Error]] = List.newBuilder val (names, funcs) = renamer.rename(prog) @@ -105,8 +107,8 @@ def compile( writer.writeTo(backend(typedProg), outputPath) *> logger.info(s"Success: ${outputPath.toAbsolutePath}") - def processProgram(contents: String, outDir: Path): IO[Int] = - frontend(contents) match { + def processProgram(contents: String, file: File, outDir: Path): IO[Int] = + frontend(contents, file) match { case Left(errors) => val code = errors.map(err => err.exitCode).toList.min given errorContent: String = contents @@ -127,7 +129,7 @@ def compile( for { contents <- readSourceFile _ <- logAction(s"Compiling file: ${filePath.toAbsolutePath}") - exitCode <- processProgram(contents, outputDir.getOrElse(filePath.getParent)) + exitCode <- processProgram(contents, filePath.toFile, outputDir.getOrElse(filePath.getParent)) } yield exitCode } diff --git a/src/main/wacc/frontend/parser.scala b/src/main/wacc/frontend/parser.scala index 9f3fa40..3b8ead4 100644 --- a/src/main/wacc/frontend/parser.scala +++ b/src/main/wacc/frontend/parser.scala @@ -1,10 +1,11 @@ package wacc +import java.io.File import parsley.Result import parsley.Parsley import parsley.Parsley.{atomic, many, notFollowedBy, pure, unit} import parsley.combinator.{countSome, sepBy, option} -import parsley.expr.{precedence, SOps, InfixL, InfixN, InfixR, Prefix, Atoms} +import parsley.expr.{precedence, SOps, InfixL, InfixN, /*InfixR,*/ Prefix, Atoms} import parsley.errors.combinator._ import parsley.errors.patterns.VerifiedErrors import parsley.syntax.zipped._ @@ -52,13 +53,30 @@ object parser { implicit val builder: ErrorBuilder[String] = new DefaultErrorBuilder with LexToken { def tokens = errTokens } - def parse(input: String): Result[String, PartialProgram] = parser.parse(input) + def parse(input: String): Result[String, File => PartialProgram] = parser.parse(input) private val parser = lexer.fully(``) + private type FParsley[A] = Parsley[File => A] + + private def fParsley[A](p: Parsley[A]): FParsley[A] = + p map { a => file => a } + + private def fList[A](p: Parsley[List[File => A]]): FParsley[List[A]] = + p map { l => file => l.map(_(file)) } + + private def fNonEmptyList[A](p: Parsley[NonEmptyList[File => A]]): FParsley[NonEmptyList[A]] = + p map { l => file => l.map(_(file)) } + + private def fPair[A, B](p: Parsley[(File => A, File => B)]): FParsley[(A, B)] = + p map { case (a, b) => file => (a(file), b(file)) } + + private def fOption[A](p: Parsley[Option[File => A]]): FParsley[Option[A]] = + p map { l => file => l.map(_(file)) } + // Expressions - private lazy val ``: Parsley[Expr] = precedence { - SOps(InfixR)(Or from "||") +: - SOps(InfixR)(And from "&&") +: + private lazy val ``: FParsley[Expr] = precedence { + // SOps(InfixR)(Or from "||") +: + // SOps(InfixR)(And from "&&") +: SOps(InfixN)(Eq from "==", Neq from "!=") +: SOps(InfixN)( Less from "<", @@ -83,7 +101,7 @@ object parser { } // Atoms - private lazy val ``: Atoms[Expr6] = Atoms( + private lazy val ``: Atoms[File => Expr6] = Atoms( IntLiter(integer).label("integer literal"), BoolLiter(("true" as true) | ("false" as false)).label("boolean literal"), CharLiter(charLit).label("character literal"), @@ -92,24 +110,24 @@ object parser { ``, Parens("(" ~> `` <~ ")") ) - private val `` = StrLiter(stringLit) - private val `` = + private lazy val `` = StrLiter(stringLit) + private lazy val `` = Ident(ident) | some("*" | "&").verifiedExplain("pointer operators are not allowed") private lazy val `` = (`` <~ ("(".verifiedExplain( "functions can only be called using 'call' keyword" ) | unit)) <**> (`` identity) - private val `` = ArrayElem(some("[" ~> `` <~ "]")) + private lazy val `` = ArrayElem(fNonEmptyList(some("[" ~> `` <~ "]"))) // Types - private lazy val ``: Parsley[Type] = + private lazy val ``: FParsley[Type] = (`` | (`` ~> ``)) <**> (`` identity) private val `` = (IntType from "int") | (BoolType from "bool") | (CharType from "char") | (StringType from "string") private lazy val `` = - ArrayType(countSome("[" ~> "]")) + ArrayType(fParsley(countSome("[" ~> "]"))) private val `` = "pair" - private val ``: Parsley[PairType] = PairType( + private val ``: FParsley[PairType] = PairType( "(" ~> `` <~ ",", `` <~ ")" ) @@ -117,7 +135,7 @@ object parser { (`` <**> (`` identity)) | ((UntypedPairType from ``) <**> ((`` <**> ``) - .map(arr => (_: UntypedPairType) => arr) identity)) + .map(arr => (_: File => UntypedPairType) => arr) identity)) /* Statements Atomic is used in two places here: @@ -129,25 +147,25 @@ object parser { concern. */ private lazy val `` = PartialProgram( - many(``), + fList(many(``)), `` ) private lazy val `` = Import( "import" ~> ``, - "(" ~> sepBy1(``, ",") <~ ")" + "(" ~> fNonEmptyList(sepBy1(``, ",")) <~ ")" ) private lazy val `` = ``.label("import file name") private lazy val `` = ImportedFunc( ``.label("imported function name"), - option("as" ~> ``).label("imported function alias") + fOption(option("as" ~> ``)).label("imported function alias") ) private lazy val `` = Program( "begin" ~> ( - many( - atomic( + fList(many( + fPair(atomic( ``.label("function declaration") <~> `` <~ "(" - ) <**> `` - ).label("function declaration") | + )) <**> `` + ).label("function declaration")) | atomic(`` <~ "(").verifiedExplain("function declaration is missing return type") ), ``.label( @@ -156,17 +174,18 @@ object parser { ) private lazy val `` = FuncDecl( - sepBy(``, ",") <~ ")" <~ "is", - ``.guardAgainst { - case stmts if !stmts.isReturning => Seq("all functions must end in a returning statement") - } <~ "end" + fPair((fList(sepBy(``, ",")) <~ ")" <~ "is") <~> + (``.guardAgainst { + // TODO: passing in an arbitrary file works but is ugly + case stmts if !(stmts(File("."))).isReturning => Seq("all functions must end in a returning statement") + } <~ "end")) ) private lazy val `` = Param(``, ``) - private lazy val ``: Parsley[NonEmptyList[Stmt]] = - ( + private lazy val ``: FParsley[NonEmptyList[Stmt]] = + fNonEmptyList(( ``.label("main program body"), (many(";" ~> ``.label("statement after ';'"))) Nil - ).zipped(NonEmptyList.apply) + ).zipped(NonEmptyList.apply)) private lazy val `` = (Skip from "skip") @@ -174,8 +193,8 @@ object parser { | Free("free" ~> ``.labelAndExplain(LabelType.Expr)) | Return("return" ~> ``.labelAndExplain(LabelType.Expr)) | Exit("exit" ~> ``.labelAndExplain(LabelType.Expr)) - | Print("print" ~> ``.labelAndExplain(LabelType.Expr), pure(false)) - | Print("println" ~> ``.labelAndExplain(LabelType.Expr), pure(true)) + | Print("print" ~> ``.labelAndExplain(LabelType.Expr), fParsley(pure(false))) + | Print("println" ~> ``.labelAndExplain(LabelType.Expr), fParsley(pure(true))) | If( "if" ~> ``.labelWithType(LabelType.Expr) <~ "then", `` <~ "else", @@ -199,9 +218,9 @@ object parser { ("call" ~> ``).verifiedExplain( "function calls' results must be assigned to a variable" ) - private lazy val ``: Parsley[LValue] = + private lazy val ``: FParsley[LValue] = `` | `` - private lazy val ``: Parsley[RValue] = + private lazy val ``: FParsley[RValue] = `` | NewPair( "newpair" ~> "(" ~> `` <~ ",", @@ -210,13 +229,13 @@ object parser { `` | Call( "call" ~> `` <~ "(", - sepBy(``, ",") <~ ")" + fList(sepBy(``, ",")) <~ ")" ) | ``.labelWithType(LabelType.Expr) private lazy val `` = Fst("fst" ~> ``.label("valid pair")) | Snd("snd" ~> ``.label("valid pair")) private lazy val `` = ArrayLiter( - "[" ~> sepBy(``, ",") <~ "]" + "[" ~> fList(sepBy(``, ",")) <~ "]" ) extension (stmts: NonEmptyList[Stmt]) { From 6904aa37e4c36527a1f701a410c9c7914cd8e8e0 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 13:31:47 +0000 Subject: [PATCH 23/32] style: scala format --- src/main/wacc/Main.scala | 3 ++- src/main/wacc/frontend/ast.scala | 29 ++++++++++++++------ src/main/wacc/frontend/parser.scala | 41 ++++++++++++++++++----------- 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 68688bc..add3eec 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -69,7 +69,8 @@ val outputOpt: Opts[Option[Path]] = .orNone def frontend( - contents: String, file: File + contents: String, + file: File ): Either[NonEmptyList[Error], microWacc.Program] = parser.parse(contents) match { case Failure(msg) => Left(NonEmptyList.one(Error.SyntaxError(msg))) diff --git a/src/main/wacc/frontend/ast.scala b/src/main/wacc/frontend/ast.scala index 4a397a9..a291070 100644 --- a/src/main/wacc/frontend/ast.scala +++ b/src/main/wacc/frontend/ast.scala @@ -134,7 +134,8 @@ object ast { extends Type with PairElemType object ArrayType extends ParserBridgePos2Chain[Int, Type, ArrayType] { - def apply(dimensions: Int, elemType: Type)(pos: Position): ArrayType = ArrayType(elemType, dimensions)(pos) + def apply(dimensions: Int, elemType: Type)(pos: Position): ArrayType = + ArrayType(elemType, dimensions)(pos) } case class PairType(fst: PairElemType, snd: PairElemType)(val pos: Position) extends Type object PairType extends ParserBridgePos2[PairElemType, PairElemType, PairType] @@ -171,9 +172,12 @@ object ast { object FuncDecl extends ParserBridgePos2Chain[ (List[Param], NonEmptyList[Stmt]), - ((Type, Ident)), FuncDecl + ((Type, Ident)), + FuncDecl ] { - def apply(paramsBody: (List[Param], NonEmptyList[Stmt]), retTyName: (Type, Ident))(pos: Position): FuncDecl = + def apply(paramsBody: (List[Param], NonEmptyList[Stmt]), retTyName: (Type, Ident))( + pos: Position + ): FuncDecl = new FuncDecl(retTyName._1, retTyName._2, paramsBody._1, paramsBody._2)(pos) } @@ -261,15 +265,19 @@ object ast { a => file => this.apply(a(file))(Position(pos._1, pos._2, file)) } - trait ParserBridgePos2Chain[-A, -B, +C] extends ParserSingletonBridgePos[(File => A) => (File => B) => File => C] { + trait ParserBridgePos2Chain[-A, -B, +C] + extends ParserSingletonBridgePos[(File => A) => (File => B) => File => C] { def apply(a: A, b: B)(pos: Position): C - def apply(a: Parsley[File =>A]): Parsley[(File => B) => File => C] = error(ap1(pos.map(con), a)) + def apply(a: Parsley[File => A]): Parsley[(File => B) => File => C] = error( + ap1(pos.map(con), a) + ) override final def con(pos: (Int, Int)): (File => A) => (File => B) => File => C = a => b => file => this.apply(a(file), b(file))(Position(pos._1, pos._2, file)) } - trait ParserBridgePos2[-A, -B, +C] extends ParserSingletonBridgePos[(File => A, File => B) => File => C] { + trait ParserBridgePos2[-A, -B, +C] + extends ParserSingletonBridgePos[(File => A, File => B) => File => C] { def apply(a: A, b: B)(pos: Position): C def apply(a: Parsley[File => A], b: => Parsley[File => B]): Parsley[File => C] = error( ap2(pos.map(con), a, b) @@ -279,9 +287,14 @@ object ast { (a, b) => file => this.apply(a(file), b(file))(Position(pos._1, pos._2, file)) } - trait ParserBridgePos3[-A, -B, -C, +D] extends ParserSingletonBridgePos[(File => A, File => B, File => C) => File => D] { + trait ParserBridgePos3[-A, -B, -C, +D] + extends ParserSingletonBridgePos[(File => A, File => B, File => C) => File => D] { def apply(a: A, b: B, c: C)(pos: Position): D - def apply(a: Parsley[File => A], b: => Parsley[File => B], c: => Parsley[File => C]): Parsley[File => D] = error( + def apply( + a: Parsley[File => A], + b: => Parsley[File => B], + c: => Parsley[File => C] + ): Parsley[File => D] = error( ap3(pos.map(con), a, b, c) ) diff --git a/src/main/wacc/frontend/parser.scala b/src/main/wacc/frontend/parser.scala index 3b8ead4..c9d36f3 100644 --- a/src/main/wacc/frontend/parser.scala +++ b/src/main/wacc/frontend/parser.scala @@ -76,8 +76,8 @@ object parser { // Expressions private lazy val ``: FParsley[Expr] = precedence { // SOps(InfixR)(Or from "||") +: - // SOps(InfixR)(And from "&&") +: - SOps(InfixN)(Eq from "==", Neq from "!=") +: + // SOps(InfixR)(And from "&&") +: + SOps(InfixN)(Eq from "==", Neq from "!=") +: SOps(InfixN)( Less from "<", LessEq from "<=", @@ -161,11 +161,15 @@ object parser { ) private lazy val `` = Program( "begin" ~> ( - fList(many( - fPair(atomic( - ``.label("function declaration") <~> `` <~ "(" - )) <**> `` - ).label("function declaration")) | + fList( + many( + fPair( + atomic( + ``.label("function declaration") <~> `` <~ "(" + ) + ) <**> `` + ).label("function declaration") + ) | atomic(`` <~ "(").verifiedExplain("function declaration is missing return type") ), ``.label( @@ -174,18 +178,23 @@ object parser { ) private lazy val `` = FuncDecl( - fPair((fList(sepBy(``, ",")) <~ ")" <~ "is") <~> - (``.guardAgainst { - // TODO: passing in an arbitrary file works but is ugly - case stmts if !(stmts(File("."))).isReturning => Seq("all functions must end in a returning statement") - } <~ "end")) + fPair( + (fList(sepBy(``, ",")) <~ ")" <~ "is") <~> + (``.guardAgainst { + // TODO: passing in an arbitrary file works but is ugly + case stmts if !(stmts(File("."))).isReturning => + Seq("all functions must end in a returning statement") + } <~ "end") + ) ) private lazy val `` = Param(``, ``) private lazy val ``: FParsley[NonEmptyList[Stmt]] = - fNonEmptyList(( - ``.label("main program body"), - (many(";" ~> ``.label("statement after ';'"))) Nil - ).zipped(NonEmptyList.apply)) + fNonEmptyList( + ( + ``.label("main program body"), + (many(";" ~> ``.label("statement after ';'"))) Nil + ).zipped(NonEmptyList.apply) + ) private lazy val `` = (Skip from "skip") From 0497dd34a06f777f2bb9a0f37d3fbfdd9925fb53 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 13:37:17 +0000 Subject: [PATCH 24/32] fix: use GOps to avoid scala error --- src/main/wacc/frontend/parser.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/wacc/frontend/parser.scala b/src/main/wacc/frontend/parser.scala index c9d36f3..232474f 100644 --- a/src/main/wacc/frontend/parser.scala +++ b/src/main/wacc/frontend/parser.scala @@ -5,7 +5,7 @@ import parsley.Result import parsley.Parsley import parsley.Parsley.{atomic, many, notFollowedBy, pure, unit} import parsley.combinator.{countSome, sepBy, option} -import parsley.expr.{precedence, SOps, InfixL, InfixN, /*InfixR,*/ Prefix, Atoms} +import parsley.expr.{precedence, SOps, InfixL, InfixN, InfixR, Prefix, Atoms} import parsley.errors.combinator._ import parsley.errors.patterns.VerifiedErrors import parsley.syntax.zipped._ @@ -14,6 +14,7 @@ import cats.data.NonEmptyList import parsley.errors.DefaultErrorBuilder import parsley.errors.ErrorBuilder import parsley.errors.tokenextractors.LexToken +import parsley.expr.GOps object parser { import lexer.implicits.implicitSymbol @@ -75,9 +76,9 @@ object parser { // Expressions private lazy val ``: FParsley[Expr] = precedence { - // SOps(InfixR)(Or from "||") +: - // SOps(InfixR)(And from "&&") +: - SOps(InfixN)(Eq from "==", Neq from "!=") +: + GOps(InfixR)(Or from "||") +: + GOps(InfixR)(And from "&&") +: + SOps(InfixN)(Eq from "==", Neq from "!=") +: SOps(InfixN)( Less from "<", LessEq from "<=", From 67e85688b20b14d999191760e7e846350886cde7 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 14:03:53 +0000 Subject: [PATCH 25/32] refactor: fMap to replace fOption, fList and fNonEmptyList --- src/main/wacc/frontend/parser.scala | 30 +++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/main/wacc/frontend/parser.scala b/src/main/wacc/frontend/parser.scala index 232474f..ce9283c 100644 --- a/src/main/wacc/frontend/parser.scala +++ b/src/main/wacc/frontend/parser.scala @@ -10,11 +10,13 @@ import parsley.errors.combinator._ import parsley.errors.patterns.VerifiedErrors import parsley.syntax.zipped._ import parsley.cats.combinator.{some, sepBy1} +import cats.syntax.all._ import cats.data.NonEmptyList import parsley.errors.DefaultErrorBuilder import parsley.errors.ErrorBuilder import parsley.errors.tokenextractors.LexToken import parsley.expr.GOps +import cats.Functor object parser { import lexer.implicits.implicitSymbol @@ -62,17 +64,11 @@ object parser { private def fParsley[A](p: Parsley[A]): FParsley[A] = p map { a => file => a } - private def fList[A](p: Parsley[List[File => A]]): FParsley[List[A]] = - p map { l => file => l.map(_(file)) } - - private def fNonEmptyList[A](p: Parsley[NonEmptyList[File => A]]): FParsley[NonEmptyList[A]] = - p map { l => file => l.map(_(file)) } - private def fPair[A, B](p: Parsley[(File => A, File => B)]): FParsley[(A, B)] = p map { case (a, b) => file => (a(file), b(file)) } - private def fOption[A](p: Parsley[Option[File => A]]): FParsley[Option[A]] = - p map { l => file => l.map(_(file)) } + private def fMap[A, F[_]: Functor](p: Parsley[F[File => A]]): FParsley[F[A]] = + p map { funcs => file => funcs.map(_(file)) } // Expressions private lazy val ``: FParsley[Expr] = precedence { @@ -118,7 +114,7 @@ object parser { (`` <~ ("(".verifiedExplain( "functions can only be called using 'call' keyword" ) | unit)) <**> (`` identity) - private lazy val `` = ArrayElem(fNonEmptyList(some("[" ~> `` <~ "]"))) + private lazy val `` = ArrayElem(fMap(some("[" ~> `` <~ "]"))) // Types private lazy val ``: FParsley[Type] = @@ -148,21 +144,21 @@ object parser { concern. */ private lazy val `` = PartialProgram( - fList(many(``)), + fMap(many(``)), `` ) private lazy val `` = Import( "import" ~> ``, - "(" ~> fNonEmptyList(sepBy1(``, ",")) <~ ")" + "(" ~> fMap(sepBy1(``, ",")) <~ ")" ) private lazy val `` = ``.label("import file name") private lazy val `` = ImportedFunc( ``.label("imported function name"), - fOption(option("as" ~> ``)).label("imported function alias") + fMap(option("as" ~> ``)).label("imported function alias") ) private lazy val `` = Program( "begin" ~> ( - fList( + fMap( many( fPair( atomic( @@ -180,7 +176,7 @@ object parser { private lazy val `` = FuncDecl( fPair( - (fList(sepBy(``, ",")) <~ ")" <~ "is") <~> + (fMap(sepBy(``, ",")) <~ ")" <~ "is") <~> (``.guardAgainst { // TODO: passing in an arbitrary file works but is ugly case stmts if !(stmts(File("."))).isReturning => @@ -190,7 +186,7 @@ object parser { ) private lazy val `` = Param(``, ``) private lazy val ``: FParsley[NonEmptyList[Stmt]] = - fNonEmptyList( + fMap( ( ``.label("main program body"), (many(";" ~> ``.label("statement after ';'"))) Nil @@ -239,13 +235,13 @@ object parser { `` | Call( "call" ~> `` <~ "(", - fList(sepBy(``, ",")) <~ ")" + fMap(sepBy(``, ",")) <~ ")" ) | ``.labelWithType(LabelType.Expr) private lazy val `` = Fst("fst" ~> ``.label("valid pair")) | Snd("snd" ~> ``.label("valid pair")) private lazy val `` = ArrayLiter( - "[" ~> fList(sepBy(``, ",")) <~ "]" + "[" ~> fMap(sepBy(``, ",")) <~ "]" ) extension (stmts: NonEmptyList[Stmt]) { From 00df2dc54641ab20b6a6ae2e02501d1e0ee27635 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 14:57:35 +0000 Subject: [PATCH 26/32] feat: filenames in errors --- src/main/wacc/Main.scala | 4 ++-- src/main/wacc/frontend/Error.scala | 23 +++++++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index add3eec..48166c3 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -73,7 +73,7 @@ def frontend( file: File ): Either[NonEmptyList[Error], microWacc.Program] = parser.parse(contents) match { - case Failure(msg) => Left(NonEmptyList.one(Error.SyntaxError(msg))) + case Failure(msg) => Left(NonEmptyList.one(Error.SyntaxError(file, msg))) case Success(fn) => val ast.PartialProgram(_, prog) = fn(file) given errors: mutable.Builder[Error, List[Error]] = List.newBuilder @@ -118,7 +118,7 @@ def compile( _ <- 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 ${filePath.toAbsolutePath}:\n$errorMsg") + println(s"Compilation failed for ${file.toPath.toRealPath()}:\n$errorMsg") ) } yield code diff --git a/src/main/wacc/frontend/Error.scala b/src/main/wacc/frontend/Error.scala index e515494..ed113ae 100644 --- a/src/main/wacc/frontend/Error.scala +++ b/src/main/wacc/frontend/Error.scala @@ -2,6 +2,7 @@ package wacc import wacc.ast.Position import wacc.types._ +import java.io.File private val SYNTAX_ERROR = 100 private val SEMANTIC_ERROR = 200 @@ -18,13 +19,13 @@ enum Error { case TypeMismatch(pos: Position, expected: SemType, got: SemType, msg: String) case InternalError(pos: Position, msg: String) - case SyntaxError(msg: String) + case SyntaxError(file: File, msg: String) } extension (e: Error) { def exitCode: Int = e match { - case Error.SyntaxError(_) => SYNTAX_ERROR - case _ => SEMANTIC_ERROR + case Error.SyntaxError(_, _) => SYNTAX_ERROR + case _ => SEMANTIC_ERROR } } @@ -38,12 +39,22 @@ extension (e: Error) { def formatError(error: Error)(using errorContent: String): String = { val sb = new StringBuilder() + /** Format the file of an error + * + * @param file + * File of the error + */ + def formatFile(file: File): Unit = { + sb.append(s"File: ${file.toPath.toRealPath()}\n") + } + /** Function to format the position of an error * * @param pos * Position of the error */ def formatPosition(pos: Position): Unit = { + formatFile(pos.file) sb.append(s"(line ${pos.line}, column ${pos.column}):\n") } @@ -67,7 +78,7 @@ def formatError(error: Error)(using errorContent: String): String = { } error match { - case Error.SyntaxError(_) => + case Error.SyntaxError(_, _) => sb.append("Syntax error:\n") case _ => sb.append("Semantic error:\n") @@ -105,11 +116,11 @@ def formatError(error: Error)(using errorContent: String): String = { formatPosition(pos) sb.append(s"Internal error: $msg") formatHighlight(pos, 1) - case Error.SyntaxError(msg) => + case Error.SyntaxError(file, msg) => + formatFile(file) sb.append(msg) sb.append("\n") } sb.toString() - } From 8d8df3357d31a2f9083373741f063b4aac9160d8 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 18:39:11 +0000 Subject: [PATCH 27/32] refactor: use getCanonicalPath instead of toRealPath --- src/main/wacc/Main.scala | 2 +- src/main/wacc/frontend/Error.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 48166c3..3fef6dc 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -118,7 +118,7 @@ def compile( _ <- 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.toPath.toRealPath()}:\n$errorMsg") + println(s"Compilation failed for ${file.getCanonicalPath}:\n$errorMsg") ) } yield code diff --git a/src/main/wacc/frontend/Error.scala b/src/main/wacc/frontend/Error.scala index ed113ae..7c2ab7e 100644 --- a/src/main/wacc/frontend/Error.scala +++ b/src/main/wacc/frontend/Error.scala @@ -45,7 +45,7 @@ def formatError(error: Error)(using errorContent: String): String = { * File of the error */ def formatFile(file: File): Unit = { - sb.append(s"File: ${file.toPath.toRealPath()}\n") + sb.append(s"File: ${file.getCanonicalPath}\n") } /** Function to format the position of an error From c73b073f23693b9f48baa8b3bbd171d7ffeda317 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 20:45:57 +0000 Subject: [PATCH 28/32] feat: initial attempt at renamer parallelisation --- src/main/wacc/frontend/ast.scala | 9 +- src/main/wacc/frontend/renamer.scala | 463 ++++++++++++++++++--------- src/main/wacc/frontend/types.scala | 6 +- 3 files changed, 318 insertions(+), 160 deletions(-) diff --git a/src/main/wacc/frontend/ast.scala b/src/main/wacc/frontend/ast.scala index a291070..7e15c60 100644 --- a/src/main/wacc/frontend/ast.scala +++ b/src/main/wacc/frontend/ast.scala @@ -32,7 +32,10 @@ object ast { object StrLiter extends ParserBridgePos1Atom[String, StrLiter] case class PairLiter()(val pos: Position) extends Expr6 object PairLiter extends ParserBridgePos0[PairLiter] - case class Ident(v: String, var uid: Int = -1)(val pos: Position) extends Expr6 with LValue + case class Ident(v: String, var guid: Int = -1, var ty: types.RenamerType = types.?)( + val pos: Position + ) extends Expr6 + with LValue object Ident extends ParserBridgePos1Atom[String, Ident] { def apply(v: String)(pos: Position): Ident = new Ident(v)(pos) } @@ -186,7 +189,9 @@ object ast { /* ============================ STATEMENTS ============================ */ - sealed trait Stmt + sealed trait Stmt { + val pos: Position + } case class Skip()(val pos: Position) extends Stmt object Skip extends ParserBridgePos0[Skip] case class VarDecl(varType: Type, name: Ident, value: RValue)(val pos: Position) extends Stmt diff --git a/src/main/wacc/frontend/renamer.scala b/src/main/wacc/frontend/renamer.scala index b281283..748ed81 100644 --- a/src/main/wacc/frontend/renamer.scala +++ b/src/main/wacc/frontend/renamer.scala @@ -1,6 +1,19 @@ package wacc +import java.io.File import scala.collection.mutable +import cats.effect.IO +import cats.syntax.all._ +import cats.implicits._ +import cats.data.Chain +import cats.Foldable +import cats.Functor +import cats.data.NonEmptyList +import parsley.{Failure, Success} +import cats.data.NonEmptyChain +import cats.NonEmptyParallel + +private val MAIN = "$main" object renamer { import ast._ @@ -11,209 +24,347 @@ object renamer { case Var } + private case class ScopeKey(path: String, name: String, identType: IdentType) + private case class ScopeValue(id: Ident, public: Boolean) + private class Scope( - val current: mutable.Map[(String, IdentType), Ident], - val parent: Map[(String, IdentType), Ident] + private val current: mutable.Map[ScopeKey, ScopeValue], + private val parent: Map[ScopeKey, ScopeValue], + guidStart: Int = 0, + val guidInc: Int = 1 ) { + private var guid = guidStart + private var immutable = false + + private def nextGuid(): Int = { + val id = guid + guid += guidInc + id + } + + private def verifyMutable(): Unit = { + if (immutable) throw new IllegalStateException("Cannot modify an immutable scope") + } /** Create a new scope with the current scope as its parent. + * + * To be used for single-threaded applications. * * @return * A new scope with an empty current scope, and this scope flattened into the parent scope. */ - def subscope: Scope = - Scope(mutable.Map.empty, Map.empty.withDefault(current.withDefault(parent))) + def withSubscope[T](f: Scope => T): T = { + val subscope = Scope(mutable.Map.empty, Map.empty.withDefault(current.withDefault(parent)), guid, guidInc) + immutable = true + val result = f(subscope) + guid = subscope.guid // Sync GUID + immutable = false + result + } + + /** Create new scopes with the current scope as its parent and GUID numbering adjusted + * correctly. + * + * This will permanently mark the current scope as immutable, for thread safety. + * + * To be used for multi-threaded applications. + * + * @return + * New scopes with an empty current scope, and this scope flattened into the parent scope. + */ + def subscopes(n: Int): Seq[Scope] = { + verifyMutable() + immutable = true + (0 until n).map { i => + Scope( + mutable.Map.empty, + Map.empty.withDefault(current.withDefault(parent)), + guid + i * guidInc, + guidInc * n + ) + } + } /** Attempt to add a new identifier to the current scope. If the identifier already exists in * the current scope, add an error to the error list. * - * @param ty - * The semantic type of the variable identifier, or function identifier type. * @param name * The name of the identifier. - * @param globalNames - * The global map of identifiers to semantic types - the identifier will be added to this - * map. - * @param globalNumbering - * The global map of identifier names to the number of times they have been declared - will - * used to rename this identifier, and will be incremented. - * @param errors - * The list of errors to append to. + * @return + * An error, if one occurred. */ - def add(ty: SemType | FuncType, name: Ident)(using - globalNames: mutable.Map[Ident, SemType], - globalFuncs: mutable.Map[Ident, FuncType], - globalNumbering: mutable.Map[String, Int], - errors: mutable.Builder[Error, List[Error]] - ) = { - val identType = ty match { + def add(name: Ident, public: Boolean = false): Chain[Error] = { + verifyMutable() + val path = name.pos.file.getCanonicalPath + val identType = name.ty match { case _: SemType => IdentType.Var case _: FuncType => IdentType.Func } - current.get((name.v, identType)) match { - case Some(Ident(_, uid)) => - errors += Error.DuplicateDeclaration(name) - name.uid = uid + val key = ScopeKey(path, name.v, identType) + current.get(key) match { + case Some(ScopeValue(Ident(_, id, _), _)) => + name.guid = id + Chain.one(Error.DuplicateDeclaration(name)) case None => - val uid = globalNumbering.getOrElse(name.v, 0) - name.uid = uid - current((name.v, identType)) = name - - ty match { - case semType: SemType => - globalNames(name) = semType - case funcType: FuncType => - globalFuncs(name) = funcType - } - globalNumbering(name.v) = uid + 1 + name.guid = nextGuid() + current(key) = ScopeValue(name, public) + Chain.empty } } - private def get(name: String, identType: IdentType): Option[Ident] = + /** Attempt to add a new identifier as an alias to another to the existing scope. + * + * @param alias + * The (new) alias identifier. + * @param orig + * The (existing) original identifier. + * + * @return + * An error, if one occurred. + */ + def addAlias(alias: Ident, orig: ScopeValue, public: Boolean = false): Chain[Error] = { + verifyMutable() + val path = alias.pos.file.getCanonicalPath + val identType = alias.ty match { + case _: SemType => IdentType.Var + case _: FuncType => IdentType.Func + } + val key = ScopeKey(path, alias.v, identType) + current.get(key) match { + case Some(ScopeValue(Ident(_, id, _), _)) => + alias.guid = id + Chain.one(Error.DuplicateDeclaration(alias)) + case None => + alias.guid = nextGuid() + current(key) = ScopeValue(orig.id, public) + Chain.empty + } + } + + def get(path: String, name: String, identType: IdentType): Option[ScopeValue] = // Unfortunately map defaults only work with `.apply()`, which throws an error when the key is not found. // Neither is there a way to check whether a default exists, so we have to use a try-catch. try { - Some(current.withDefault(parent)((name, identType))) + Some(current.withDefault(parent)(ScopeKey(path, name, identType))) } catch { case _: NoSuchElementException => None } - def getVar(name: String): Option[Ident] = get(name, IdentType.Var) - def getFunc(name: String): Option[Ident] = get(name, IdentType.Func) + def getVar(name: Ident): Option[Ident] = get(name.pos.file.getCanonicalPath, name.v, IdentType.Var).map(_.id) + def getFunc(name: Ident): Option[Ident] = get(name.pos.file.getCanonicalPath, name.v, IdentType.Func).map(_.id) } - /** Check scoping of all variables and functions in the program. Also generate semantic types for - * all identifiers. - * - * @param prog - * AST of the program - * @param errors - * List of errors to append to - * @return - * Map of all (renamed) identifies to their semantic types - */ - def rename(prog: Program)(using - errors: mutable.Builder[Error, List[Error]] - ): (Map[Ident, SemType], Map[Ident, FuncType]) = { - given globalNames: mutable.Map[Ident, SemType] = mutable.Map.empty - given globalFuncs: mutable.Map[Ident, FuncType] = mutable.Map.empty - given globalNumbering: mutable.Map[String, Int] = mutable.Map.empty - val scope = Scope(mutable.Map.empty, Map.empty) + private def prepareGlobalScope(partialProg: PartialProgram)(using scope: Scope): IO[(FuncDecl, Chain[FuncDecl], Chain[Error])] = { + def readImportFile(file: File): IO[String] = + IO.blocking(os.read(os.Path(file.getCanonicalPath))) + + def prepareImport(contents: String, file: File)(using scope: Scope): IO[(Chain[FuncDecl], Chain[Error])] = { + parser.parse(contents) match { + case Failure(msg) => + IO.pure(Chain.empty, Chain.one(Error.SyntaxError(file, msg))) + case Success(fn) => + val partialProg = fn(file) + for { + (main, chunks, errors) <- prepareGlobalScope(partialProg) + } yield (main +: chunks, errors) + } + + } + + val PartialProgram(imports, prog) = partialProg + + // First prepare this file's functions... val Program(funcs, main) = prog - funcs - // First add all function declarations to the scope - .map { case FuncDecl(retType, name, params, body) => + val (funcChunks, funcErrors) = funcs.foldLeft((Chain.empty[FuncDecl], Chain.empty[Error])) { + case ((chunks, errors), func@FuncDecl(retType, name, params, body)) => val paramTypes = params.map { param => val paramType = SemType(param.paramType) + param.name.ty = paramType paramType } - scope.add(FuncType(SemType(retType), paramTypes), name) - (params zip paramTypes, body) - } - // Only then rename the function bodies - // (functions can call one-another regardless of order of declaration) - .foreach { case (params, body) => - val functionScope = scope.subscope - params.foreach { case (param, paramType) => - functionScope.add(paramType, param.name) - } - body.toList.foreach(rename(functionScope.subscope)) // body can shadow function params - } - main.toList.foreach(rename(scope)) - (globalNames.toMap, globalFuncs.toMap) + name.ty = FuncType(SemType(retType), paramTypes) + (chunks :+ func, errors ++ scope.add(name, public = true)) + } + // ...and main body. + val mainBodyIdent = Ident(MAIN, ty = FuncType(?, Nil))(prog.pos) + val mainBodyErrors = scope.add(mainBodyIdent, public = false) + val mainBodyChunk = FuncDecl(IntType()(prog.pos), mainBodyIdent, Nil, main)(prog.pos) + + // Now handle imports + val file = prog.pos.file + val preparedImports = imports.foldLeftM[IO, (Chain[FuncDecl], Chain[Error])]((Chain.empty[FuncDecl], Chain.empty[Error])) { + case ((chunks, errors), Import(name, funcs)) => + val importFile = File(file.getParent, name.v) + for { + contents <- readImportFile(importFile) + (importChunks, importErrors) <- prepareImport(contents, importFile) + importAliasErrors = funcs.foldMap { case ImportedFunc(srcName, aliasName) => + scope.get(importFile.getCanonicalPath, srcName.v, IdentType.Func) match { + case Some(src) if src.public => scope.addAlias(aliasName, src) + case _ => Chain.one(Error.UndefinedFunction(srcName)) + } + + } + } yield (chunks ++ importChunks, errors ++ importErrors) + } + + for { + (importChunks, importErrors) <- preparedImports + allChunks = importChunks ++ funcChunks + allErrors = importErrors ++ funcErrors ++ mainBodyErrors + } yield (mainBodyChunk, allChunks, allErrors) } + /** Check scoping of all variables and flatten a program. Also generates semantic types and parses + * any imported files. + * + * @param partialProg + * AST of the program + * @return + * (flattenedProg, errors) + */ + def rename(partialProg: PartialProgram): IO[(Program, Chain[Error])] = { + given scope: Scope = Scope(mutable.Map.empty, Map.empty) + for { + (main, chunks, errors) <- prepareGlobalScope(partialProg) + toRename = (main +: chunks).toList + res = (toRename zip scope.subscopes(toRename.size)).parTraverse { case (func@FuncDecl(retType, name, params, body), subscope) => + val paramErrors = params.foldMap { param => subscope.add(param.name) } + val bodyErrors = subscope.withSubscope { s => body.foldMap(rename(s)) } + paramErrors ++ bodyErrors + } + } yield (partialProg.self, errors) + } + + // /** Check scoping of all variables and functions in the program. Also generate semantic types for + // * all identifiers. + // * + // * @param prog + // * AST of the program + // * @param errors + // * List of errors to append to + // * @return + // * Map of all (renamed) identifies to their semantic types + // */ + // def rename(prog: Program)(using + // errors: mutable.Builder[Error, List[Error]] + // ): (Map[Ident, SemType], Map[Ident, FuncType]) = { + // given globalNames: mutable.Map[Ident, SemType] = mutable.Map.empty + // given globalFuncs: mutable.Map[Ident, FuncType] = mutable.Map.empty + // given globalNumbering: mutable.Map[String, Int] = mutable.Map.empty + // val scope = Scope(mutable.Map.empty, Map.empty) + // val Program(funcs, main) = prog + // funcs + // // First add all function declarations to the scope + // .map { case FuncDecl(retType, name, params, body) => + // val paramTypes = params.map { param => + // val paramType = SemType(param.paramType) + // paramType + // } + // scope.add(FuncType(SemType(retType), paramTypes), name) + // (params zip paramTypes, body) + // } + // // Only then rename the function bodies + // // (functions can call one-another regardless of order of declaration) + // .foreach { case (params, body) => + // val functionScope = scope.subscope + // params.foreach { case (param, paramType) => + // functionScope.add(paramType, param.name) + // } + // body.toList.foreach(rename(functionScope.subscope)) // body can shadow function params + // } + // main.toList.foreach(rename(scope)) + // (globalNames.toMap, globalFuncs.toMap) + // } + /** Check scoping of all identifies in a given AST node. * * @param scope * The current scope and flattened parent scope. * @param node * The AST node. - * @param globalNames - * The global map of identifiers to semantic types - renamed identifiers will be added to this - * map. - * @param globalNumbering - * The global map of identifier names to the number of times they have been declared - used and - * updated during identifier renaming. - * @param errors */ - private def rename(scope: Scope)( - node: Ident | Stmt | LValue | RValue | Expr - )(using - globalNames: mutable.Map[Ident, SemType], - globalFuncs: mutable.Map[Ident, FuncType], - globalNumbering: mutable.Map[String, Int], - errors: mutable.Builder[Error, List[Error]] - ): Unit = node match { - // These cases are more interesting because the involve making subscopes - // or modifying the current scope. - case VarDecl(synType, name, value) => { - // Order matters here. Variable isn't declared until after the value is evaluated. - rename(scope)(value) - // Attempt to add the new variable to the current scope. - scope.add(SemType(synType), name) - } - case If(cond, thenStmt, elseStmt) => { - rename(scope)(cond) - // then and else both have their own scopes - thenStmt.toList.foreach(rename(scope.subscope)) - elseStmt.toList.foreach(rename(scope.subscope)) - } - case While(cond, body) => { - rename(scope)(cond) - // while bodies have their own scopes - body.toList.foreach(rename(scope.subscope)) - } - // begin-end blocks have their own scopes - case Block(body) => body.toList.foreach(rename(scope.subscope)) + private def rename(scope: Scope)(node: Ident | Stmt | LValue | RValue | Expr): Chain[Error] = + node match { + // These cases are more interes/globting because the involve making subscopes + // or modifying the current scope. + case VarDecl(synType, name, value) => { + // Order matters here. Variable isn't declared until after the value is evaluated. + val errors = rename(scope)(value) + // Attempt to add the new variable to the current scope. + name.ty = SemType(synType) + errors ++ scope.add(name) + } + case If(cond, thenStmt, elseStmt) => { + val condErrors = rename(scope)(cond) + // then and else both have their own scopes + val thenErrors = scope.withSubscope(s => thenStmt.foldMap(rename(s))) + val elseErrors = scope.withSubscope(s => elseStmt.foldMap(rename(s))) + condErrors ++ thenErrors ++ elseErrors + } + case While(cond, body) => { + val condErrors = rename(scope)(cond) + // while bodies have their own scopes + val bodyErrors = scope.withSubscope(s => body.foldMap(rename(s))) + condErrors ++ bodyErrors + } + // begin-end blocks have their own scopes + case Block(body) => scope.withSubscope(s => body.foldMap(rename(s))) - // These cases are simpler, mostly just recursive calls to rename() - case Assign(lhs, value) => { - // Variables may be reassigned with their value in the rhs, so order doesn't matter here. - rename(scope)(lhs) - rename(scope)(value) - } - case Read(lhs) => rename(scope)(lhs) - case Free(expr) => rename(scope)(expr) - case Return(expr) => rename(scope)(expr) - case Exit(expr) => rename(scope)(expr) - case Print(expr, _) => rename(scope)(expr) - case NewPair(fst, snd) => { - rename(scope)(fst) - rename(scope)(snd) - } - case Call(name, args) => { - scope.getFunc(name.v) match { - case Some(Ident(_, uid)) => name.uid = uid - case None => - errors += Error.UndefinedFunction(name) - scope.add(FuncType(?, args.map(_ => ?)), name) + // These cases are simpler, mostly just recursive calls to rename() + case Assign(lhs, value) => { + // Variables may be reassigned with their value in the rhs, so order doesn't matter here. + rename(scope)(lhs) ++ rename(scope)(value) } - args.foreach(rename(scope)) - } - case Fst(elem) => rename(scope)(elem) - case Snd(elem) => rename(scope)(elem) - case ArrayLiter(elems) => elems.foreach(rename(scope)) - case ArrayElem(name, indices) => { - rename(scope)(name) - indices.toList.foreach(rename(scope)) - } - case Parens(expr) => rename(scope)(expr) - case op: UnaryOp => rename(scope)(op.x) - case op: BinaryOp => { - rename(scope)(op.x) - rename(scope)(op.y) - } - // Default to variables. Only `call` uses IdentType.Func. - case id: Ident => { - scope.getVar(id.v) match { - case Some(Ident(_, uid)) => id.uid = uid - case None => - errors += Error.UndeclaredVariable(id) - scope.add(?, id) + case Read(lhs) => rename(scope)(lhs) + case Free(expr) => rename(scope)(expr) + case Return(expr) => rename(scope)(expr) + case Exit(expr) => rename(scope)(expr) + case Print(expr, _) => rename(scope)(expr) + case NewPair(fst, snd) => { + rename(scope)(fst) ++ rename(scope)(snd) } + case Call(name, args) => { + val nameErrors = scope.getFunc(name) match { + case Some(Ident(_, guid, ty)) => + name.ty = ty + name.guid = guid + Chain.empty + case None => + name.ty = FuncType(?, args.map(_ => ?)) + scope.add(name) + Chain.one(Error.UndefinedFunction(name)) + } + val argsErrors = args.foldMap(rename(scope)) + nameErrors ++ argsErrors + } + case Fst(elem) => rename(scope)(elem) + case Snd(elem) => rename(scope)(elem) + case ArrayLiter(elems) => elems.foldMap(rename(scope)) + case ArrayElem(name, indices) => { + val nameErrors = rename(scope)(name) + val indicesErrors = indices.foldMap(rename(scope)) + nameErrors ++ indicesErrors + } + case Parens(expr) => rename(scope)(expr) + case op: UnaryOp => rename(scope)(op.x) + case op: BinaryOp => { + rename(scope)(op.x) ++ rename(scope)(op.y) + } + // Default to variables. Only `call` uses IdentType.Func. + case id: Ident => { + scope.getVar(id) match { + case Some(Ident(_, guid, ty)) => + id.ty = ty + id.guid = guid + Chain.empty + case None => + id.ty = ? + scope.add(id) + Chain.one(Error.UndeclaredVariable(id)) + } + } + // These literals cannot contain identifies, exit immediately. + case IntLiter(_) | BoolLiter(_) | CharLiter(_) | StrLiter(_) | PairLiter() | Skip() => Chain.empty } - // These literals cannot contain identifies, exit immediately. - case IntLiter(_) | BoolLiter(_) | CharLiter(_) | StrLiter(_) | PairLiter() | Skip() => () - } } diff --git a/src/main/wacc/frontend/types.scala b/src/main/wacc/frontend/types.scala index 549d8a1..5251396 100644 --- a/src/main/wacc/frontend/types.scala +++ b/src/main/wacc/frontend/types.scala @@ -3,7 +3,9 @@ package wacc object types { import ast._ - sealed trait SemType { + sealed trait RenamerType + + sealed trait SemType extends RenamerType { override def toString(): String = this match { case KnownType.Int => "int" case KnownType.Bool => "bool" @@ -41,5 +43,5 @@ object types { } } - case class FuncType(returnType: SemType, params: List[SemType]) + case class FuncType(returnType: SemType, params: List[SemType]) extends RenamerType } From ee54a1201c68b892fc494518531da96c4ce3d652 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 20:47:56 +0000 Subject: [PATCH 29/32] fix: return proper AST from renamer --- src/main/wacc/frontend/renamer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/wacc/frontend/renamer.scala b/src/main/wacc/frontend/renamer.scala index 748ed81..3d5c917 100644 --- a/src/main/wacc/frontend/renamer.scala +++ b/src/main/wacc/frontend/renamer.scala @@ -233,7 +233,7 @@ object renamer { val bodyErrors = subscope.withSubscope { s => body.foldMap(rename(s)) } paramErrors ++ bodyErrors } - } yield (partialProg.self, errors) + } yield (Program(chunks.toList, main.body)(main.pos), errors) } // /** Check scoping of all variables and functions in the program. Also generate semantic types for From 6e592e7d9b6b73f446cbe0dbbac28b5665e4fd68 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 22:24:41 +0000 Subject: [PATCH 30/32] feat: functional single-threaded imports --- extension/examples/valid/imports/basic.wacc | 1 + .../examples/valid/imports/manyMains.wacc | 1 - .../{mutliFunc.wacc => multiFunc.wacc} | 0 src/main/wacc/Main.scala | 56 ++++--- src/main/wacc/backend/LabelGenerator.scala | 2 +- src/main/wacc/frontend/Error.scala | 22 +-- src/main/wacc/frontend/ast.scala | 2 +- src/main/wacc/frontend/renamer.scala | 152 +++++++++--------- src/main/wacc/frontend/typeChecker.scala | 73 ++++----- src/test/wacc/examples.scala | 12 +- 10 files changed, 166 insertions(+), 155 deletions(-) rename extension/examples/valid/imports/{mutliFunc.wacc => multiFunc.wacc} (100%) diff --git a/extension/examples/valid/imports/basic.wacc b/extension/examples/valid/imports/basic.wacc index aae959b..d34a34a 100644 --- a/extension/examples/valid/imports/basic.wacc +++ b/extension/examples/valid/imports/basic.wacc @@ -2,6 +2,7 @@ # Output: # -33 +# # Exit: # 0 diff --git a/extension/examples/valid/imports/manyMains.wacc b/extension/examples/valid/imports/manyMains.wacc index d52997f..fc3bc7c 100644 --- a/extension/examples/valid/imports/manyMains.wacc +++ b/extension/examples/valid/imports/manyMains.wacc @@ -2,7 +2,6 @@ # Output: # 15 -# 0 # -33 # 0 # -33 diff --git a/extension/examples/valid/imports/mutliFunc.wacc b/extension/examples/valid/imports/multiFunc.wacc similarity index 100% rename from extension/examples/valid/imports/mutliFunc.wacc rename to extension/examples/valid/imports/multiFunc.wacc diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 3fef6dc..e78d4bd 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -71,21 +71,23 @@ val outputOpt: Opts[Option[Path]] = def frontend( contents: String, file: File -): Either[NonEmptyList[Error], microWacc.Program] = +): IO[Either[NonEmptyList[Error], microWacc.Program]] = parser.parse(contents) match { - case Failure(msg) => Left(NonEmptyList.one(Error.SyntaxError(file, msg))) + case Failure(msg) => IO.pure(Left(NonEmptyList.one(Error.SyntaxError(file, msg)))) case Success(fn) => - val ast.PartialProgram(_, prog) = fn(file) + val partialProg = fn(file) 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) + for { + (prog, renameErrors) <- renamer.rename(partialProg) + _ = errors.addAll(renameErrors.toList) + typedProg = typeChecker.check(prog, errors) - NonEmptyList.fromList(errors.result) match { - case Some(errors) => Left(errors) - case None => Right(typedProg) - } + res = NonEmptyList.fromList(errors.result) match { + case Some(errors) => Left(errors) + case None => Right(typedProg) + } + } yield res } def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] = @@ -109,23 +111,25 @@ def compile( logger.info(s"Success: ${outputPath.toAbsolutePath}") def processProgram(contents: String, file: File, outDir: Path): IO[Int] = - frontend(contents, file) match { - case Left(errors) => - val code = errors.map(err => err.exitCode).toList.min - given errorContent: String = contents - 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 + for { + frontendResult <- frontend(contents, file) + 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) - } + case Right(typedProg) => + val outputFile = outDir.resolve(filePath.getFileName.toString.stripSuffix(".wacc") + ".s") + writeOutputFile(typedProg, outputFile).as(SUCCESS) + } + } yield res for { contents <- readSourceFile diff --git a/src/main/wacc/backend/LabelGenerator.scala b/src/main/wacc/backend/LabelGenerator.scala index 3b5169b..29d03db 100644 --- a/src/main/wacc/backend/LabelGenerator.scala +++ b/src/main/wacc/backend/LabelGenerator.scala @@ -18,7 +18,7 @@ private class LabelGenerator { } private def getLabel(target: CallTarget | RuntimeError): String = target match { - case Ident(v, _) => s"wacc_$v" + case Ident(v, guid) => s"wacc_${v}_$guid" case Builtin(name) => s"_$name" case err: RuntimeError => s".L.${err.name}" } diff --git a/src/main/wacc/frontend/Error.scala b/src/main/wacc/frontend/Error.scala index 7c2ab7e..188e91c 100644 --- a/src/main/wacc/frontend/Error.scala +++ b/src/main/wacc/frontend/Error.scala @@ -36,7 +36,7 @@ extension (e: Error) { * @param errorContent * Contents of the file to generate code snippets */ -def formatError(error: Error)(using errorContent: String): String = { +def formatError(error: Error): String = { val sb = new StringBuilder() /** Format the file of an error @@ -66,7 +66,7 @@ def formatError(error: Error)(using errorContent: String): String = { * Size(in chars) of section to highlight */ def formatHighlight(pos: Position, size: Int): Unit = { - val lines = errorContent.split("\n") + val lines = os.read(os.Path(pos.file.getCanonicalPath)).split("\n") val preLine = if (pos.line > 1) lines(pos.line - 2) else "" val midLine = lines(pos.line - 1) val postLine = if (pos.line < lines.size) lines(pos.line) else "" @@ -87,38 +87,38 @@ def formatError(error: Error)(using errorContent: String): String = { error match { case Error.DuplicateDeclaration(ident) => formatPosition(ident.pos) - sb.append(s"Duplicate declaration of identifier ${ident.v}") + sb.append(s"Duplicate declaration of identifier ${ident.v}\n") formatHighlight(ident.pos, ident.v.length) case Error.UndeclaredVariable(ident) => formatPosition(ident.pos) - sb.append(s"Undeclared variable ${ident.v}") + sb.append(s"Undeclared variable ${ident.v}\n") formatHighlight(ident.pos, ident.v.length) case Error.UndefinedFunction(ident) => formatPosition(ident.pos) - sb.append(s"Undefined function ${ident.v}") + sb.append(s"Undefined function ${ident.v}\n") formatHighlight(ident.pos, ident.v.length) case Error.FunctionParamsMismatch(id, expected, got, funcType) => formatPosition(id.pos) - sb.append(s"Function expects $expected parameters, got $got") + sb.append(s"Function expects $expected parameters, got $got\n") sb.append( - s"(function ${id.v} has type (${funcType.params.mkString(", ")}) -> ${funcType.returnType})" + s"(function ${id.v} has type (${funcType.params.mkString(", ")}) -> ${funcType.returnType})\n" ) formatHighlight(id.pos, 1) case Error.TypeMismatch(pos, expected, got, msg) => formatPosition(pos) - sb.append(s"Type mismatch: $msg\nExpected: $expected\nGot: $got") + sb.append(s"Type mismatch: $msg\nExpected: $expected\nGot: $got\n") formatHighlight(pos, 1) case Error.SemanticError(pos, msg) => formatPosition(pos) - sb.append(msg) + sb.append(msg + "\n") formatHighlight(pos, 1) case wacc.Error.InternalError(pos, msg) => formatPosition(pos) - sb.append(s"Internal error: $msg") + sb.append(s"Internal error: $msg\n") formatHighlight(pos, 1) case Error.SyntaxError(file, msg) => formatFile(file) - sb.append(msg) + sb.append(msg + "\n") sb.append("\n") } diff --git a/src/main/wacc/frontend/ast.scala b/src/main/wacc/frontend/ast.scala index 7e15c60..e39f931 100644 --- a/src/main/wacc/frontend/ast.scala +++ b/src/main/wacc/frontend/ast.scala @@ -32,7 +32,7 @@ object ast { object StrLiter extends ParserBridgePos1Atom[String, StrLiter] case class PairLiter()(val pos: Position) extends Expr6 object PairLiter extends ParserBridgePos0[PairLiter] - case class Ident(v: String, var guid: Int = -1, var ty: types.RenamerType = types.?)( + case class Ident(var v: String, var guid: Int = -1, var ty: types.RenamerType = types.?)( val pos: Position ) extends Expr6 with LValue diff --git a/src/main/wacc/frontend/renamer.scala b/src/main/wacc/frontend/renamer.scala index 3d5c917..bbce1bb 100644 --- a/src/main/wacc/frontend/renamer.scala +++ b/src/main/wacc/frontend/renamer.scala @@ -6,12 +6,8 @@ import cats.effect.IO import cats.syntax.all._ import cats.implicits._ import cats.data.Chain -import cats.Foldable -import cats.Functor import cats.data.NonEmptyList import parsley.{Failure, Success} -import cats.data.NonEmptyChain -import cats.NonEmptyParallel private val MAIN = "$main" @@ -54,7 +50,8 @@ object renamer { * A new scope with an empty current scope, and this scope flattened into the parent scope. */ def withSubscope[T](f: Scope => T): T = { - val subscope = Scope(mutable.Map.empty, Map.empty.withDefault(current.withDefault(parent)), guid, guidInc) + val subscope = + Scope(mutable.Map.empty, Map.empty.withDefault(current.withDefault(parent)), guid, guidInc) immutable = true val result = f(subscope) guid = subscope.guid // Sync GUID @@ -64,7 +61,7 @@ object renamer { /** Create new scopes with the current scope as its parent and GUID numbering adjusted * correctly. - * + * * This will permanently mark the current scope as immutable, for thread safety. * * To be used for multi-threaded applications. @@ -115,10 +112,10 @@ object renamer { /** Attempt to add a new identifier as an alias to another to the existing scope. * * @param alias - * The (new) alias identifier. + * The (new) alias identifier. * @param orig - * The (existing) original identifier. - * + * The (existing) original identifier. + * * @return * An error, if one occurred. */ @@ -150,15 +147,21 @@ object renamer { case _: NoSuchElementException => None } - def getVar(name: Ident): Option[Ident] = get(name.pos.file.getCanonicalPath, name.v, IdentType.Var).map(_.id) - def getFunc(name: Ident): Option[Ident] = get(name.pos.file.getCanonicalPath, name.v, IdentType.Func).map(_.id) + def getVar(name: Ident): Option[Ident] = + get(name.pos.file.getCanonicalPath, name.v, IdentType.Var).map(_.id) + def getFunc(name: Ident): Option[Ident] = + get(name.pos.file.getCanonicalPath, name.v, IdentType.Func).map(_.id) } - private def prepareGlobalScope(partialProg: PartialProgram)(using scope: Scope): IO[(FuncDecl, Chain[FuncDecl], Chain[Error])] = { + private def prepareGlobalScope( + partialProg: PartialProgram + )(using scope: Scope): IO[(FuncDecl, Chain[FuncDecl], Chain[Error])] = { def readImportFile(file: File): IO[String] = IO.blocking(os.read(os.Path(file.getCanonicalPath))) - def prepareImport(contents: String, file: File)(using scope: Scope): IO[(Chain[FuncDecl], Chain[Error])] = { + def prepareImport(contents: String, file: File)(using + scope: Scope + ): IO[(Chain[FuncDecl], Chain[Error])] = { parser.parse(contents) match { case Failure(msg) => IO.pure(Chain.empty, Chain.one(Error.SyntaxError(file, msg))) @@ -168,15 +171,27 @@ object renamer { (main, chunks, errors) <- prepareGlobalScope(partialProg) } yield (main +: chunks, errors) } - } + def addImportsToScope(importFile: File, funcs: NonEmptyList[ImportedFunc])(using + scope: Scope + ): Chain[Error] = + funcs.foldMap { case ImportedFunc(srcName, aliasName) => + scope.get(importFile.getCanonicalPath, srcName.v, IdentType.Func) match { + case Some(src) if src.public => + aliasName.ty = src.id.ty + scope.addAlias(aliasName, src) + case _ => + Chain.one(Error.UndefinedFunction(srcName)) + } + } + val PartialProgram(imports, prog) = partialProg // First prepare this file's functions... val Program(funcs, main) = prog val (funcChunks, funcErrors) = funcs.foldLeft((Chain.empty[FuncDecl], Chain.empty[Error])) { - case ((chunks, errors), func@FuncDecl(retType, name, params, body)) => + case ((chunks, errors), func @ FuncDecl(retType, name, params, body)) => val paramTypes = params.map { param => val paramType = SemType(param.paramType) param.name.ty = paramType @@ -192,20 +207,49 @@ object renamer { // Now handle imports val file = prog.pos.file - val preparedImports = imports.foldLeftM[IO, (Chain[FuncDecl], Chain[Error])]((Chain.empty[FuncDecl], Chain.empty[Error])) { - case ((chunks, errors), Import(name, funcs)) => - val importFile = File(file.getParent, name.v) + val preparedImports = imports.foldLeftM[IO, (Chain[FuncDecl], Chain[Error])]( + (Chain.empty[FuncDecl], Chain.empty[Error]) + ) { case ((chunks, errors), Import(name, funcs)) => + val importFile = File(file.getParent, name.v) + if (!importFile.exists()) { + IO.pure( + ( + chunks, + errors :+ Error.SemanticError( + name.pos, + s"File not found: ${importFile.getCanonicalPath}" + ) + ) + ) + } else if (!importFile.canRead()) { + IO.pure( + ( + chunks, + errors :+ Error.SemanticError( + name.pos, + s"File not readable: ${importFile.getCanonicalPath}" + ) + ) + ) + } else if (importFile.getCanonicalPath == file.getCanonicalPath) { + IO.pure( + ( + chunks, + errors :+ Error.SemanticError( + name.pos, + s"Cannot import self: ${importFile.getCanonicalPath}" + ) + ) + ) + } else if (scope.get(importFile.getCanonicalPath, MAIN, IdentType.Func).isDefined) { + IO.pure(chunks, errors ++ addImportsToScope(importFile, funcs)) + } else { for { contents <- readImportFile(importFile) (importChunks, importErrors) <- prepareImport(contents, importFile) - importAliasErrors = funcs.foldMap { case ImportedFunc(srcName, aliasName) => - scope.get(importFile.getCanonicalPath, srcName.v, IdentType.Func) match { - case Some(src) if src.public => scope.addAlias(aliasName, src) - case _ => Chain.one(Error.UndefinedFunction(srcName)) - } - - } + importAliasErrors = addImportsToScope(importFile, funcs) } yield (chunks ++ importChunks, errors ++ importErrors) + } } for { @@ -228,55 +272,15 @@ object renamer { for { (main, chunks, errors) <- prepareGlobalScope(partialProg) toRename = (main +: chunks).toList - res = (toRename zip scope.subscopes(toRename.size)).parTraverse { case (func@FuncDecl(retType, name, params, body), subscope) => - val paramErrors = params.foldMap { param => subscope.add(param.name) } - val bodyErrors = subscope.withSubscope { s => body.foldMap(rename(s)) } - paramErrors ++ bodyErrors + res = (toRename zip scope.subscopes(toRename.size)).foldMap { + case (func @ FuncDecl(retType, name, params, body), subscope) => + val paramErrors = params.foldMap { param => subscope.add(param.name) } + val bodyErrors = subscope.withSubscope { s => body.foldMap(rename(s)) } + paramErrors ++ bodyErrors } - } yield (Program(chunks.toList, main.body)(main.pos), errors) + } yield (Program(chunks.toList, main.body)(main.pos), errors ++ res) } - // /** Check scoping of all variables and functions in the program. Also generate semantic types for - // * all identifiers. - // * - // * @param prog - // * AST of the program - // * @param errors - // * List of errors to append to - // * @return - // * Map of all (renamed) identifies to their semantic types - // */ - // def rename(prog: Program)(using - // errors: mutable.Builder[Error, List[Error]] - // ): (Map[Ident, SemType], Map[Ident, FuncType]) = { - // given globalNames: mutable.Map[Ident, SemType] = mutable.Map.empty - // given globalFuncs: mutable.Map[Ident, FuncType] = mutable.Map.empty - // given globalNumbering: mutable.Map[String, Int] = mutable.Map.empty - // val scope = Scope(mutable.Map.empty, Map.empty) - // val Program(funcs, main) = prog - // funcs - // // First add all function declarations to the scope - // .map { case FuncDecl(retType, name, params, body) => - // val paramTypes = params.map { param => - // val paramType = SemType(param.paramType) - // paramType - // } - // scope.add(FuncType(SemType(retType), paramTypes), name) - // (params zip paramTypes, body) - // } - // // Only then rename the function bodies - // // (functions can call one-another regardless of order of declaration) - // .foreach { case (params, body) => - // val functionScope = scope.subscope - // params.foreach { case (param, paramType) => - // functionScope.add(paramType, param.name) - // } - // body.toList.foreach(rename(functionScope.subscope)) // body can shadow function params - // } - // main.toList.foreach(rename(scope)) - // (globalNames.toMap, globalFuncs.toMap) - // } - /** Check scoping of all identifies in a given AST node. * * @param scope @@ -326,7 +330,8 @@ object renamer { } case Call(name, args) => { val nameErrors = scope.getFunc(name) match { - case Some(Ident(_, guid, ty)) => + case Some(Ident(realName, guid, ty)) => + name.v = realName name.ty = ty name.guid = guid Chain.empty @@ -365,6 +370,7 @@ object renamer { } } // These literals cannot contain identifies, exit immediately. - case IntLiter(_) | BoolLiter(_) | CharLiter(_) | StrLiter(_) | PairLiter() | Skip() => Chain.empty + case IntLiter(_) | BoolLiter(_) | CharLiter(_) | StrLiter(_) | PairLiter() | Skip() => + Chain.empty } } diff --git a/src/main/wacc/frontend/typeChecker.scala b/src/main/wacc/frontend/typeChecker.scala index a628b69..6f5804b 100644 --- a/src/main/wacc/frontend/typeChecker.scala +++ b/src/main/wacc/frontend/typeChecker.scala @@ -8,13 +8,8 @@ object typeChecker { import wacc.types._ case class TypeCheckerCtx( - globalNames: Map[ast.Ident, SemType], - globalFuncs: Map[ast.Ident, FuncType], errors: mutable.Builder[Error, List[Error]] ) { - def typeOf(ident: ast.Ident): SemType = globalNames(ident) - def funcType(ident: ast.Ident): FuncType = globalFuncs(ident) - def error(err: Error): SemType = errors += err ? @@ -99,18 +94,17 @@ object typeChecker { * The type checker context which includes the global names and functions, and an errors * builder. */ - def check(prog: ast.Program)(using - ctx: TypeCheckerCtx - ): microWacc.Program = + def check(prog: ast.Program, errors: mutable.Builder[Error, List[Error]]): microWacc.Program = + given ctx: TypeCheckerCtx = TypeCheckerCtx(errors) microWacc.Program( // Ignore function syntax types for return value and params, since those have been converted // to SemTypes by the renamer. prog.funcs.map { case ast.FuncDecl(_, name, params, stmts) => - val FuncType(retType, paramTypes) = ctx.funcType(name) + val FuncType(retType, paramTypes) = name.ty.asInstanceOf[FuncType] microWacc.FuncDecl( - microWacc.Ident(name.v, name.uid)(retType), + microWacc.Ident(name.v, name.guid)(retType), params.zip(paramTypes).map { case (ast.Param(_, ident), ty) => - microWacc.Ident(ident.v, ident.uid)(ty) + microWacc.Ident(ident.v, ident.guid)(ty) }, stmts.toList .flatMap( @@ -134,15 +128,20 @@ object typeChecker { ): List[microWacc.Stmt] = stmt match { // Ignore the type of the variable, since it has been converted to a SemType by the renamer. case ast.VarDecl(_, name, value) => - val expectedTy = ctx.typeOf(name) + val expectedTy = name.ty val typedValue = checkValue( value, Constraint.Is( - expectedTy, + expectedTy.asInstanceOf[SemType], s"variable ${name.v} must be assigned a value of type $expectedTy" ) ) - List(microWacc.Assign(microWacc.Ident(name.v, name.uid)(expectedTy), typedValue)) + List( + microWacc.Assign( + microWacc.Ident(name.v, name.guid)(expectedTy.asInstanceOf[SemType]), + typedValue + ) + ) case ast.Assign(lhs, rhs) => val lhsTyped = checkLValue(lhs, Constraint.Unconstrained) val rhsTyped = @@ -315,7 +314,7 @@ object typeChecker { KnownType.Pair(fstTyped.ty, sndTyped.ty).satisfies(constraint, l.pos) ) case ast.Call(id, args) => - val funcTy @ FuncType(retTy, paramTys) = ctx.funcType(id) + val funcTy @ FuncType(retTy, paramTys) = id.ty.asInstanceOf[FuncType] if (args.length != paramTys.length) { ctx.error(Error.FunctionParamsMismatch(id, paramTys.length, args.length, funcTy)) } @@ -324,7 +323,7 @@ object typeChecker { val argsTyped = args.zip(paramTys).map { case (arg, paramTy) => checkValue(arg, Constraint.Is(paramTy, s"argument type mismatch in function ${id.v}")) } - microWacc.Call(microWacc.Ident(id.v, id.uid)(retTy.satisfies(constraint, id.pos)), argsTyped) + microWacc.Call(microWacc.Ident(id.v, id.guid)(retTy.satisfies(constraint, id.pos)), argsTyped) // Unary operators case ast.Negate(x) => @@ -416,30 +415,32 @@ object typeChecker { private def checkLValue(value: ast.LValue, constraint: Constraint)(using ctx: TypeCheckerCtx ): microWacc.LValue = value match { - case id @ ast.Ident(name, uid) => - microWacc.Ident(name, uid)(ctx.typeOf(id).satisfies(constraint, id.pos)) + case id @ ast.Ident(name, guid, ty) => + microWacc.Ident(name, guid)(ty.asInstanceOf[SemType].satisfies(constraint, id.pos)) case ast.ArrayElem(id, indices) => - val arrayTy = ctx.typeOf(id) - val (elemTy, indicesTyped) = indices.mapAccumulate(arrayTy) { (acc, elem) => - val idxTyped = checkValue(elem, Constraint.Is(KnownType.Int, "array index must be an int")) - val next = acc match { - case KnownType.Array(innerTy) => innerTy - case ? => ? // we can keep indexing an unknown type - case nonArrayTy => - ctx.error( - Error.TypeMismatch( - elem.pos, - KnownType.Array(?), - acc, - "cannot index into a non-array" + val arrayTy = id.ty.asInstanceOf[SemType] + val (elemTy, indicesTyped) = indices.mapAccumulate(arrayTy.asInstanceOf[SemType]) { + (acc, elem) => + val idxTyped = + checkValue(elem, Constraint.Is(KnownType.Int, "array index must be an int")) + val next = acc match { + case KnownType.Array(innerTy) => innerTy + case ? => ? // we can keep indexing an unknown type + case nonArrayTy => + ctx.error( + Error.TypeMismatch( + elem.pos, + KnownType.Array(?), + acc, + "cannot index into a non-array" + ) ) - ) - ? - } - (next, idxTyped) + ? + } + (next, idxTyped) } val firstArrayElem = microWacc.ArrayElem( - microWacc.Ident(id.v, id.uid)(arrayTy), + microWacc.Ident(id.v, id.guid)(arrayTy), indicesTyped.head )(elemTy.satisfies(constraint, value.pos)) val arrayElem = indicesTyped.tail.foldLeft(firstArrayElem) { (acc, idx) => diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index 13a3352..a0f4564 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -101,16 +101,16 @@ class ParallelExamplesSpec extends AsyncFreeSpec with AsyncIOSpec with BeforeAnd private def fileIsPendingFrontend(filename: String): Boolean = List( - "^.*extension/examples/invalid/syntax/imports/importBadSyntax.*$", - "^.*extension/examples/invalid/semantics/imports.*$", - "^.*extension/examples/valid/imports.*$" + // "^.*extension/examples/invalid/syntax/imports/importBadSyntax.*$", + // "^.*extension/examples/invalid/semantics/imports.*$", + // "^.*extension/examples/valid/imports.*$" ).exists(filename.matches) private def fileIsPendingBackend(filename: String): Boolean = List( - "^.*extension/examples/invalid/syntax/imports.*$", - "^.*extension/examples/invalid/semantics/imports.*$", - "^.*extension/examples/valid/imports.*$" + // "^.*extension/examples/invalid/syntax/imports.*$", + // "^.*extension/examples/invalid/semantics/imports.*$", + // "^.*extension/examples/valid/imports.*$" ).exists(filename.matches) private def extractInput(contents: List[String]): String = From a3895dca2c36c3560924547986fcd7848f96ade9 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Thu, 13 Mar 2025 22:26:56 +0000 Subject: [PATCH 31/32] style: scala format --- src/main/wacc/backend/LabelGenerator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/wacc/backend/LabelGenerator.scala b/src/main/wacc/backend/LabelGenerator.scala index 29d03db..fd0006f 100644 --- a/src/main/wacc/backend/LabelGenerator.scala +++ b/src/main/wacc/backend/LabelGenerator.scala @@ -18,7 +18,7 @@ private class LabelGenerator { } private def getLabel(target: CallTarget | RuntimeError): String = target match { - case Ident(v, guid) => s"wacc_${v}_$guid" + case Ident(v, guid) => s"wacc_${v}_$guid" case Builtin(name) => s"_$name" case err: RuntimeError => s".L.${err.name}" } From 68211fd8777f0e617a573be06d9d8efbc54ae88e Mon Sep 17 00:00:00 2001 From: Jonny Date: Thu, 13 Mar 2025 23:00:28 +0000 Subject: [PATCH 32/32] feat: parallelised the renamer --- src/main/wacc/frontend/renamer.scala | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/wacc/frontend/renamer.scala b/src/main/wacc/frontend/renamer.scala index bbce1bb..4893d42 100644 --- a/src/main/wacc/frontend/renamer.scala +++ b/src/main/wacc/frontend/renamer.scala @@ -267,18 +267,24 @@ object renamer { * @return * (flattenedProg, errors) */ + private def renameFunction(funcScopePair: (FuncDecl, Scope)): IO[Chain[Error]] = { + val (FuncDecl(_, _, params, body), subscope) = funcScopePair + val paramErrors = params.foldMap(param => subscope.add(param.name)) + IO(subscope.withSubscope { s => body.foldMap(rename(s)) }) + .map(bodyErrors => paramErrors ++ bodyErrors) + } + def rename(partialProg: PartialProgram): IO[(Program, Chain[Error])] = { given scope: Scope = Scope(mutable.Map.empty, Map.empty) + for { - (main, chunks, errors) <- prepareGlobalScope(partialProg) + (main, chunks, globalErrors) <- prepareGlobalScope(partialProg) toRename = (main +: chunks).toList - res = (toRename zip scope.subscopes(toRename.size)).foldMap { - case (func @ FuncDecl(retType, name, params, body), subscope) => - val paramErrors = params.foldMap { param => subscope.add(param.name) } - val bodyErrors = subscope.withSubscope { s => body.foldMap(rename(s)) } - paramErrors ++ bodyErrors - } - } yield (Program(chunks.toList, main.body)(main.pos), errors ++ res) + allErrors <- toRename + .zip(scope.subscopes(toRename.size)) + .parFoldMapA(renameFunction) + // .map(x => x.combineAll) + } yield (Program(chunks.toList, main.body)(main.pos), globalErrors ++ allErrors) } /** Check scoping of all identifies in a given AST node.