feat: add option flag, greedy compilation of multiple files, and refactor to...
Merge request lab2425_spring/WACC_37!41 Co-authored-by: Gleb Koval <gleb@koval.net> Co-authored-by: Jonny <j.sinteix@gmail.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,3 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
wacc-examples/
|
wacc-examples/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
|||||||
2
compile
2
compile
@@ -4,6 +4,6 @@
|
|||||||
# but do *not* change its name.
|
# but do *not* change its name.
|
||||||
|
|
||||||
# feel free to adjust to suit the specific internal flags of your compiler
|
# feel free to adjust to suit the specific internal flags of your compiler
|
||||||
./wacc-compiler "$@"
|
./wacc-compiler --output . "$@"
|
||||||
|
|
||||||
exit $?
|
exit $?
|
||||||
|
|||||||
10
extension/examples/invalid/semantics/badWacc.wacc
Normal file
10
extension/examples/invalid/semantics/badWacc.wacc
Normal file
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "./doesNotExist.wacc" (main)
|
||||||
|
|
||||||
|
begin
|
||||||
|
int result = call main() ;
|
||||||
|
exit result
|
||||||
|
end
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "../../../valid/sum.wacc" (mult)
|
||||||
|
|
||||||
|
begin
|
||||||
|
int result = call mult(3, 2) ;
|
||||||
|
exit result
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "./importBadSem.wacc" (sum)
|
||||||
|
|
||||||
|
begin
|
||||||
|
int result = call sum(1, 2) ;
|
||||||
|
exit result
|
||||||
|
end
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "../../../valid/imports/basic.wacc" (sum)
|
||||||
|
|
||||||
|
begin
|
||||||
|
int result = call sum(3, 2) ;
|
||||||
|
exit result
|
||||||
|
end
|
||||||
6
extension/examples/invalid/syntax/badWacc.wacc
Normal file
6
extension/examples/invalid/syntax/badWacc.wacc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
int main() is
|
||||||
|
println "Hello World!" ;
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
skip
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import "../../../valid/sum.wacc" ()
|
||||||
|
|
||||||
|
begin
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "./importBadSyntax.wacc" (sum)
|
||||||
|
|
||||||
|
begin
|
||||||
|
int result = call sum(1, 2) ;
|
||||||
|
exit result
|
||||||
|
end
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import "../../../valid/sum.wacc" *
|
||||||
|
|
||||||
|
begin
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import "../../../valid/sum.wacc" (*)
|
||||||
|
|
||||||
|
begin
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
7
extension/examples/valid/.gitignore
vendored
Normal file
7
extension/examples/valid/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
*
|
||||||
|
|
||||||
|
!imports/
|
||||||
|
imports/*
|
||||||
|
|
||||||
|
!.gitignore
|
||||||
|
!*.wacc
|
||||||
22
extension/examples/valid/imports/alias.wacc
Normal file
22
extension/examples/valid/imports/alias.wacc
Normal file
@@ -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
|
||||||
21
extension/examples/valid/imports/basic.wacc
Normal file
21
extension/examples/valid/imports/basic.wacc
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 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
|
||||||
33
extension/examples/valid/imports/manyMains.wacc
Normal file
33
extension/examples/valid/imports/manyMains.wacc
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# import all the mains
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# 15
|
||||||
|
# -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
|
||||||
27
extension/examples/valid/imports/multiFunc.wacc
Normal file
27
extension/examples/valid/imports/multiFunc.wacc
Normal file
@@ -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
|
||||||
27
extension/examples/valid/sum.wacc
Normal file
27
extension/examples/valid/sum.wacc
Normal file
@@ -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
|
||||||
@@ -5,23 +5,18 @@
|
|||||||
//> using dep com.github.j-mie6::parsley::5.0.0-M10
|
//> 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.github.j-mie6::parsley-cats::1.5.0
|
||||||
//> using dep com.lihaoyi::os-lib::0.11.4
|
//> using dep com.lihaoyi::os-lib::0.11.4
|
||||||
//> using dep com.github.scopt::scopt::4.1.0
|
//> 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 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 test.dep org.scalatest::scalatest::3.2.19
|
||||||
|
//> using dep org.typelevel::cats-effect-testing-scalatest::1.6.0
|
||||||
|
|
||||||
// these are all sensible defaults to catch annoying issues
|
// sensible defaults for warnings and compiler checks
|
||||||
//> using options -deprecation -unchecked -feature
|
//> using options -deprecation -unchecked -feature
|
||||||
//> using options -Wimplausible-patterns -Wunused:all
|
//> using options -Wimplausible-patterns -Wunused:all
|
||||||
//> using options -Yexplicit-nulls -Wsafe-init -Xkind-projector:underscores
|
//> using options -Yexplicit-nulls -Wsafe-init -Xkind-projector:underscores
|
||||||
|
|
||||||
// these will help ensure you have access to the latest parsley releases
|
// repositories for pre-release versions if needed
|
||||||
// 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
|
|
||||||
|
|||||||
@@ -1,92 +1,166 @@
|
|||||||
package wacc
|
package wacc
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import cats.data.Chain
|
import cats.data.{Chain, NonEmptyList}
|
||||||
import parsley.{Failure, Success}
|
import parsley.{Failure, Success}
|
||||||
import scopt.OParser
|
|
||||||
import java.io.File
|
import java.nio.file.{Files, Path}
|
||||||
import java.io.PrintStream
|
import cats.syntax.all._
|
||||||
|
|
||||||
|
import cats.effect.IO
|
||||||
|
import cats.effect.ExitCode
|
||||||
|
|
||||||
|
import com.monovore.decline._
|
||||||
|
import com.monovore.decline.effect._
|
||||||
|
|
||||||
|
import org.typelevel.log4cats.slf4j.Slf4jLogger
|
||||||
|
import org.typelevel.log4cats.Logger
|
||||||
|
|
||||||
import assemblyIR as asm
|
import assemblyIR as asm
|
||||||
|
import cats.data.ValidatedNel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
case class CliConfig(
|
/*
|
||||||
file: File = new File(".")
|
TODO:
|
||||||
)
|
1) IO correctness
|
||||||
|
2) Errors can be handled more gracefully - currently, parallelised compilation is not fail fast as far as I am aware
|
||||||
|
3) splitting the file up and nicer refactoring
|
||||||
|
4) logging could be removed
|
||||||
|
5) general cleanup and comments (things like replacing home/<user> with ~ , and names of parameters and args, descriptions etc)
|
||||||
|
*/
|
||||||
|
|
||||||
val cliBuilder = OParser.builder[CliConfig]
|
private val SUCCESS = ExitCode.Success.code
|
||||||
val cliParser = {
|
private val ERROR = ExitCode.Error.code
|
||||||
import cliBuilder._
|
|
||||||
OParser.sequence(
|
given logger: Logger[IO] = Slf4jLogger.getLogger[IO]
|
||||||
programName("wacc-compiler"),
|
|
||||||
help('h', "help")
|
val logOpt: Opts[Boolean] =
|
||||||
.text("Prints this help message"),
|
Opts.flag("log", "Enable logging for additional compilation details", short = "l").orFalse
|
||||||
arg[File]("<file>")
|
|
||||||
.text("Input WACC source file")
|
def validateFile(path: Path): ValidatedNel[String, Path] = {
|
||||||
.required()
|
(for {
|
||||||
.action((f, c) => c.copy(file = f))
|
// TODO: redundant 2nd parameter :(
|
||||||
.validate(f =>
|
_ <- Either.cond(Files.exists(path), (), s"File '${path}' does not exist")
|
||||||
if (!f.exists) failure("File does not exist")
|
_ <- Either.cond(Files.isRegularFile(path), (), s"File '${path}' must be a regular file")
|
||||||
else if (!f.isFile) failure("File must be a regular file")
|
_ <- Either.cond(path.toString.endsWith(".wacc"), (), "File must have .wacc extension")
|
||||||
else if (!f.getName.endsWith(".wacc"))
|
} yield path).toValidatedNel
|
||||||
failure("File must have .wacc extension")
|
|
||||||
else success
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val filesOpt: Opts[NonEmptyList[Path]] =
|
||||||
|
Opts.arguments[Path]("files").mapValidated {
|
||||||
|
_.traverse(validateFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
val outputOpt: Opts[Option[Path]] =
|
||||||
|
Opts
|
||||||
|
.option[Path]("output", metavar = "path", help = "Output directory for compiled files.")
|
||||||
|
.validate("Must have permissions to create & access the output path") { path =>
|
||||||
|
try {
|
||||||
|
Files.createDirectories(path)
|
||||||
|
true
|
||||||
|
} catch {
|
||||||
|
case e: java.nio.file.AccessDeniedException =>
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.validate("Output path must be a directory") { path =>
|
||||||
|
Files.isDirectory(path)
|
||||||
|
}
|
||||||
|
.orNone
|
||||||
|
|
||||||
def frontend(
|
def frontend(
|
||||||
contents: String
|
contents: String,
|
||||||
)(using stdout: PrintStream): Either[microWacc.Program, Int] = {
|
file: File
|
||||||
|
): IO[Either[NonEmptyList[Error], microWacc.Program]] =
|
||||||
parser.parse(contents) match {
|
parser.parse(contents) match {
|
||||||
case Success(prog) =>
|
case Failure(msg) => IO.pure(Left(NonEmptyList.one(Error.SyntaxError(file, msg))))
|
||||||
|
case Success(fn) =>
|
||||||
|
val partialProg = fn(file)
|
||||||
given errors: mutable.Builder[Error, List[Error]] = List.newBuilder
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val s = "enter an integer to echo"
|
for {
|
||||||
|
(prog, renameErrors) <- renamer.rename(partialProg)
|
||||||
|
_ = errors.addAll(renameErrors.toList)
|
||||||
|
typedProg = typeChecker.check(prog, errors)
|
||||||
|
|
||||||
|
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] =
|
def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] =
|
||||||
asmGenerator.generateAsm(typedProg)
|
asmGenerator.generateAsm(typedProg)
|
||||||
|
|
||||||
def compile(filename: String, outFile: Option[File] = None)(using
|
def compile(
|
||||||
stdout: PrintStream = Console.out
|
filePath: Path,
|
||||||
): Int =
|
outputDir: Option[Path],
|
||||||
frontend(os.read(os.Path(filename))) match {
|
log: Boolean
|
||||||
case Left(typedProg) =>
|
): IO[Int] = {
|
||||||
val asmFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s"))
|
val logAction: String => IO[Unit] =
|
||||||
val asm = backend(typedProg)
|
if (log) logger.info(_)
|
||||||
writer.writeTo(asm, PrintStream(asmFile))
|
else (_ => IO.unit)
|
||||||
0
|
|
||||||
case Right(exitCode) => exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
def main(args: Array[String]): Unit =
|
def readSourceFile: IO[String] =
|
||||||
OParser.parse(cliParser, args, CliConfig()) match {
|
IO.blocking(os.read(os.Path(filePath)))
|
||||||
case Some(config) =>
|
|
||||||
System.exit(
|
// TODO: path, file , the names are confusing (when Path is the type but we are working with files)
|
||||||
compile(
|
def writeOutputFile(typedProg: microWacc.Program, outputPath: Path): IO[Unit] =
|
||||||
config.file.getAbsolutePath,
|
writer.writeTo(backend(typedProg), outputPath) *>
|
||||||
outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s"))
|
logger.info(s"Success: ${outputPath.toAbsolutePath}")
|
||||||
)
|
|
||||||
)
|
def processProgram(contents: String, file: File, outDir: Path): IO[Int] =
|
||||||
case None =>
|
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)
|
||||||
|
}
|
||||||
|
} yield res
|
||||||
|
|
||||||
|
for {
|
||||||
|
contents <- readSourceFile
|
||||||
|
_ <- logAction(s"Compiling file: ${filePath.toAbsolutePath}")
|
||||||
|
exitCode <- processProgram(contents, filePath.toFile, outputDir.getOrElse(filePath.getParent))
|
||||||
|
} yield exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
def compileCommandParallel(
|
||||||
|
files: NonEmptyList[Path],
|
||||||
|
log: Boolean,
|
||||||
|
outDir: Option[Path]
|
||||||
|
): IO[ExitCode] =
|
||||||
|
files
|
||||||
|
.parTraverse { file => compile(file.toAbsolutePath, outDir, log) }
|
||||||
|
.map { exitCodes =>
|
||||||
|
exitCodes.filter(_ != 0) match {
|
||||||
|
case Nil => ExitCode.Success
|
||||||
|
case errorCodes => ExitCode(errorCodes.min)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Main
|
||||||
|
extends CommandIOApp(
|
||||||
|
name = "wacc",
|
||||||
|
header = "The ultimate WACC compiler",
|
||||||
|
version = "1.0"
|
||||||
|
) {
|
||||||
|
def main: Opts[IO[ExitCode]] =
|
||||||
|
(filesOpt, logOpt, outputOpt).mapN { (files, log, outDir) =>
|
||||||
|
compileCommandParallel(files, log, outDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ private class LabelGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def getLabel(target: CallTarget | RuntimeError): String = target match {
|
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 Builtin(name) => s"_$name"
|
||||||
case err: RuntimeError => s".L.${err.name}"
|
case err: RuntimeError => s".L.${err.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,42 @@
|
|||||||
package wacc
|
package wacc
|
||||||
|
|
||||||
import java.io.PrintStream
|
import cats.effect.Resource
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.io.BufferedWriter
|
||||||
|
import java.io.FileWriter
|
||||||
import cats.data.Chain
|
import cats.data.Chain
|
||||||
|
import cats.effect.IO
|
||||||
|
|
||||||
|
import org.typelevel.log4cats.Logger
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
object writer {
|
object writer {
|
||||||
import assemblyIR._
|
import assemblyIR._
|
||||||
|
|
||||||
def writeTo(asmList: Chain[AsmLine], printStream: PrintStream): Unit = {
|
// TODO: Judging from documentation it seems as though IO.blocking is the correct choice
|
||||||
asmList.iterator.foreach(printStream.println)
|
// 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] =
|
||||||
|
bufferedWriter(outputPath).use {
|
||||||
|
writeLines(_, asmList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package wacc
|
|||||||
|
|
||||||
import wacc.ast.Position
|
import wacc.ast.Position
|
||||||
import wacc.types._
|
import wacc.types._
|
||||||
import java.io.PrintStream
|
import java.io.File
|
||||||
|
|
||||||
|
private val SYNTAX_ERROR = 100
|
||||||
|
private val SEMANTIC_ERROR = 200
|
||||||
|
|
||||||
/** Error types for semantic errors
|
/** Error types for semantic errors
|
||||||
*/
|
*/
|
||||||
@@ -15,6 +18,15 @@ enum Error {
|
|||||||
case SemanticError(pos: Position, msg: String)
|
case SemanticError(pos: Position, msg: String)
|
||||||
case TypeMismatch(pos: Position, expected: SemType, got: SemType, msg: String)
|
case TypeMismatch(pos: Position, expected: SemType, got: SemType, msg: String)
|
||||||
case InternalError(pos: Position, msg: String)
|
case InternalError(pos: Position, msg: String)
|
||||||
|
|
||||||
|
case SyntaxError(file: File, 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
|
/** Function to handle printing the details of a given semantic error
|
||||||
@@ -24,71 +36,91 @@ enum Error {
|
|||||||
* @param errorContent
|
* @param errorContent
|
||||||
* Contents of the file to generate code snippets
|
* Contents of the file to generate code snippets
|
||||||
*/
|
*/
|
||||||
def printError(error: Error)(using errorContent: String, stdout: PrintStream): Unit = {
|
def formatError(error: Error): String = {
|
||||||
stdout.println("Semantic error:")
|
val sb = new StringBuilder()
|
||||||
error match {
|
|
||||||
case Error.DuplicateDeclaration(ident) =>
|
/** Format the file of an error
|
||||||
printPosition(ident.pos)
|
*
|
||||||
stdout.println(s"Duplicate declaration of identifier ${ident.v}")
|
* @param file
|
||||||
highlight(ident.pos, ident.v.length)
|
* File of the error
|
||||||
case Error.UndeclaredVariable(ident) =>
|
*/
|
||||||
printPosition(ident.pos)
|
def formatFile(file: File): Unit = {
|
||||||
stdout.println(s"Undeclared variable ${ident.v}")
|
sb.append(s"File: ${file.getCanonicalPath}\n")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/** Function to format the position of an error
|
||||||
|
*
|
||||||
/** Function to highlight a section of code for an error message
|
* @param pos
|
||||||
*
|
* Position of the error
|
||||||
* @param pos
|
*/
|
||||||
* Position of the error
|
def formatPosition(pos: Position): Unit = {
|
||||||
* @param size
|
formatFile(pos.file)
|
||||||
* Size(in chars) of section to highlight
|
sb.append(s"(line ${pos.line}, column ${pos.column}):\n")
|
||||||
* @param errorContent
|
}
|
||||||
* Contents of the file to generate code snippets
|
|
||||||
*/
|
/** Function to highlight a section of code for an error message
|
||||||
def highlight(pos: Position, size: Int)(using errorContent: String, stdout: PrintStream): Unit = {
|
*
|
||||||
val lines = errorContent.split("\n")
|
* @param pos
|
||||||
|
* Position of the error
|
||||||
val preLine = if (pos.line > 1) lines(pos.line - 2) else ""
|
* @param size
|
||||||
val midLine = lines(pos.line - 1)
|
* Size(in chars) of section to highlight
|
||||||
val postLine = if (pos.line < lines.size) lines(pos.line) else ""
|
*/
|
||||||
val linePointer = " " * (pos.column + 2) + ("^" * (size)) + "\n"
|
def formatHighlight(pos: Position, size: Int): Unit = {
|
||||||
|
val lines = os.read(os.Path(pos.file.getCanonicalPath)).split("\n")
|
||||||
stdout.println(
|
val preLine = if (pos.line > 1) lines(pos.line - 2) else ""
|
||||||
s" >$preLine\n >$midLine\n$linePointer >$postLine"
|
val midLine = lines(pos.line - 1)
|
||||||
)
|
val postLine = if (pos.line < lines.size) lines(pos.line) else ""
|
||||||
}
|
val linePointer = " " * (pos.column + 2) + ("^" * (size)) + "\n"
|
||||||
|
|
||||||
/** Function to print the position of an error
|
sb.append(
|
||||||
*
|
s" >$preLine\n >$midLine\n$linePointer >$postLine\netscape"
|
||||||
* @param pos
|
)
|
||||||
* Position of the error
|
}
|
||||||
*/
|
|
||||||
def printPosition(pos: Position)(using stdout: PrintStream): Unit = {
|
error match {
|
||||||
stdout.println(s"(line ${pos.line}, column ${pos.column}):")
|
case Error.SyntaxError(_, _) =>
|
||||||
|
sb.append("Syntax error:\n")
|
||||||
|
case _ =>
|
||||||
|
sb.append("Semantic error:\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
error match {
|
||||||
|
case Error.DuplicateDeclaration(ident) =>
|
||||||
|
formatPosition(ident.pos)
|
||||||
|
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}\n")
|
||||||
|
formatHighlight(ident.pos, ident.v.length)
|
||||||
|
case Error.UndefinedFunction(ident) =>
|
||||||
|
formatPosition(ident.pos)
|
||||||
|
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\n")
|
||||||
|
sb.append(
|
||||||
|
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\n")
|
||||||
|
formatHighlight(pos, 1)
|
||||||
|
case Error.SemanticError(pos, msg) =>
|
||||||
|
formatPosition(pos)
|
||||||
|
sb.append(msg + "\n")
|
||||||
|
formatHighlight(pos, 1)
|
||||||
|
case wacc.Error.InternalError(pos, msg) =>
|
||||||
|
formatPosition(pos)
|
||||||
|
sb.append(s"Internal error: $msg\n")
|
||||||
|
formatHighlight(pos, 1)
|
||||||
|
case Error.SyntaxError(file, msg) =>
|
||||||
|
formatFile(file)
|
||||||
|
sb.append(msg + "\n")
|
||||||
|
sb.append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package wacc
|
package wacc
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
import parsley.Parsley
|
import parsley.Parsley
|
||||||
import parsley.generic.ErrorBridge
|
import parsley.generic.ErrorBridge
|
||||||
import parsley.ap._
|
import parsley.ap._
|
||||||
@@ -22,26 +23,42 @@ object ast {
|
|||||||
/* ============================ ATOMIC EXPRESSIONS ============================ */
|
/* ============================ ATOMIC EXPRESSIONS ============================ */
|
||||||
|
|
||||||
case class IntLiter(v: Int)(val pos: Position) extends Expr6
|
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
|
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
|
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
|
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
|
case class PairLiter()(val pos: Position) extends Expr6
|
||||||
object PairLiter extends ParserBridgePos0[PairLiter]
|
object PairLiter extends ParserBridgePos0[PairLiter]
|
||||||
case class Ident(v: String, var uid: Int = -1)(val pos: Position) extends Expr6 with LValue
|
case class Ident(var v: String, var guid: Int = -1, var ty: types.RenamerType = types.?)(
|
||||||
object Ident extends ParserBridgePos1[String, Ident] {
|
val pos: Position
|
||||||
|
) extends Expr6
|
||||||
|
with LValue
|
||||||
|
object Ident extends ParserBridgePos1Atom[String, Ident] {
|
||||||
def apply(v: String)(pos: Position): Ident = new Ident(v)(pos)
|
def apply(v: String)(pos: Position): Ident = new Ident(v)(pos)
|
||||||
}
|
}
|
||||||
case class ArrayElem(name: Ident, indices: NonEmptyList[Expr])(val pos: Position)
|
case class ArrayElem(name: Ident, indices: NonEmptyList[Expr])(val pos: Position)
|
||||||
extends Expr6
|
extends Expr6
|
||||||
with LValue
|
with LValue
|
||||||
object ArrayElem extends ParserBridgePos1[NonEmptyList[Expr], Ident => ArrayElem] {
|
object ArrayElem extends ParserBridgePos2Chain[NonEmptyList[Expr], Ident, ArrayElem] {
|
||||||
def apply(a: NonEmptyList[Expr])(pos: Position): Ident => ArrayElem =
|
def apply(indices: NonEmptyList[Expr], name: Ident)(pos: Position): ArrayElem =
|
||||||
name => ArrayElem(name, a)(pos)
|
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
|
case class Parens(expr: Expr)(val pos: Position) extends Expr6
|
||||||
object Parens extends ParserBridgePos1[Expr, Parens]
|
object Parens extends ParserBridgePos1[Expr, Parens]
|
||||||
|
|
||||||
@@ -119,8 +136,9 @@ object ast {
|
|||||||
case class ArrayType(elemType: Type, dimensions: Int)(val pos: Position)
|
case class ArrayType(elemType: Type, dimensions: Int)(val pos: Position)
|
||||||
extends Type
|
extends Type
|
||||||
with PairElemType
|
with PairElemType
|
||||||
object ArrayType extends ParserBridgePos1[Int, Type => ArrayType] {
|
object ArrayType extends ParserBridgePos2Chain[Int, Type, ArrayType] {
|
||||||
def apply(a: Int)(pos: Position): Type => ArrayType = elemType => ArrayType(elemType, a)(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
|
case class PairType(fst: PairElemType, snd: PairElemType)(val pos: Position) extends Type
|
||||||
object PairType extends ParserBridgePos2[PairElemType, PairElemType, PairType]
|
object PairType extends ParserBridgePos2[PairElemType, PairElemType, PairType]
|
||||||
@@ -131,6 +149,18 @@ object ast {
|
|||||||
|
|
||||||
/* ============================ PROGRAM STRUCTURE ============================ */
|
/* ============================ 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)
|
case class Program(funcs: List[FuncDecl], main: NonEmptyList[Stmt])(val pos: Position)
|
||||||
object Program extends ParserBridgePos2[List[FuncDecl], NonEmptyList[Stmt], Program]
|
object Program extends ParserBridgePos2[List[FuncDecl], NonEmptyList[Stmt], Program]
|
||||||
|
|
||||||
@@ -143,15 +173,15 @@ object ast {
|
|||||||
body: NonEmptyList[Stmt]
|
body: NonEmptyList[Stmt]
|
||||||
)(val pos: Position)
|
)(val pos: Position)
|
||||||
object FuncDecl
|
object FuncDecl
|
||||||
extends ParserBridgePos2[
|
extends ParserBridgePos2Chain[
|
||||||
List[Param],
|
(List[Param], NonEmptyList[Stmt]),
|
||||||
NonEmptyList[Stmt],
|
((Type, Ident)),
|
||||||
((Type, Ident)) => FuncDecl
|
FuncDecl
|
||||||
] {
|
] {
|
||||||
def apply(params: List[Param], body: NonEmptyList[Stmt])(
|
def apply(paramsBody: (List[Param], NonEmptyList[Stmt]), retTyName: (Type, Ident))(
|
||||||
pos: Position
|
pos: Position
|
||||||
): ((Type, Ident)) => FuncDecl =
|
): FuncDecl =
|
||||||
(returnType, name) => FuncDecl(returnType, name, params, body)(pos)
|
new FuncDecl(retTyName._1, retTyName._2, paramsBody._1, paramsBody._2)(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Param(paramType: Type, name: Ident)(val pos: Position)
|
case class Param(paramType: Type, name: Ident)(val pos: Position)
|
||||||
@@ -159,7 +189,9 @@ object ast {
|
|||||||
|
|
||||||
/* ============================ STATEMENTS ============================ */
|
/* ============================ STATEMENTS ============================ */
|
||||||
|
|
||||||
sealed trait Stmt
|
sealed trait Stmt {
|
||||||
|
val pos: Position
|
||||||
|
}
|
||||||
case class Skip()(val pos: Position) extends Stmt
|
case class Skip()(val pos: Position) extends Stmt
|
||||||
object Skip extends ParserBridgePos0[Skip]
|
object Skip extends ParserBridgePos0[Skip]
|
||||||
case class VarDecl(varType: Type, name: Ident, value: RValue)(val pos: Position) extends Stmt
|
case class VarDecl(varType: Type, name: Ident, value: RValue)(val pos: Position) extends Stmt
|
||||||
@@ -207,7 +239,7 @@ object ast {
|
|||||||
|
|
||||||
/* ============================ PARSER BRIDGES ============================ */
|
/* ============================ PARSER BRIDGES ============================ */
|
||||||
|
|
||||||
case class Position(line: Int, column: Int)
|
case class Position(line: Int, column: Int, file: File)
|
||||||
|
|
||||||
trait ParserSingletonBridgePos[+A] extends ErrorBridge {
|
trait ParserSingletonBridgePos[+A] extends ErrorBridge {
|
||||||
protected def con(pos: (Int, Int)): A
|
protected def con(pos: (Int, Int)): A
|
||||||
@@ -215,38 +247,63 @@ object ast {
|
|||||||
final def <#(op: Parsley[?]): Parsley[A] = this from op
|
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
|
def apply()(pos: Position): A
|
||||||
|
|
||||||
override final def con(pos: (Int, Int)): A =
|
override final def con(pos: (Int, Int)): File => A =
|
||||||
apply()(Position(pos._1, pos._2))
|
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: 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 =
|
override final def con(pos: (Int, Int)): A => File => B =
|
||||||
this.apply(_)(Position(pos._1, pos._2))
|
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: 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)
|
ap2(pos.map(con), a, b)
|
||||||
)
|
)
|
||||||
|
|
||||||
override final def con(pos: (Int, Int)): (A, B) => C =
|
override final def con(pos: (Int, Int)): (File => A, File => B) => File => C =
|
||||||
apply(_, _)(Position(pos._1, pos._2))
|
(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: 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)
|
ap3(pos.map(con), a, b, c)
|
||||||
)
|
)
|
||||||
|
|
||||||
override final def con(pos: (Int, Int)): (A, B, C) => D =
|
override final def con(pos: (Int, Int)): (File => A, File => B, File => C) => File => D =
|
||||||
apply(_, _, _)(Position(pos._1, pos._2))
|
(a, b, c) => file => apply(a(file), b(file), c(file))(Position(pos._1, pos._2, file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
package wacc
|
package wacc
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
import parsley.Result
|
import parsley.Result
|
||||||
import parsley.Parsley
|
import parsley.Parsley
|
||||||
import parsley.Parsley.{atomic, many, notFollowedBy, pure, unit}
|
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.expr.{precedence, SOps, InfixL, InfixN, InfixR, Prefix, Atoms}
|
||||||
import parsley.errors.combinator._
|
import parsley.errors.combinator._
|
||||||
import parsley.errors.patterns.VerifiedErrors
|
import parsley.errors.patterns.VerifiedErrors
|
||||||
import parsley.syntax.zipped._
|
import parsley.syntax.zipped._
|
||||||
import parsley.cats.combinator.{some}
|
import parsley.cats.combinator.{some, sepBy1}
|
||||||
|
import cats.syntax.all._
|
||||||
import cats.data.NonEmptyList
|
import cats.data.NonEmptyList
|
||||||
import parsley.errors.DefaultErrorBuilder
|
import parsley.errors.DefaultErrorBuilder
|
||||||
import parsley.errors.ErrorBuilder
|
import parsley.errors.ErrorBuilder
|
||||||
import parsley.errors.tokenextractors.LexToken
|
import parsley.errors.tokenextractors.LexToken
|
||||||
|
import parsley.expr.GOps
|
||||||
|
import cats.Functor
|
||||||
|
|
||||||
object parser {
|
object parser {
|
||||||
import lexer.implicits.implicitSymbol
|
import lexer.implicits.implicitSymbol
|
||||||
@@ -52,13 +56,24 @@ object parser {
|
|||||||
implicit val builder: ErrorBuilder[String] = new DefaultErrorBuilder with LexToken {
|
implicit val builder: ErrorBuilder[String] = new DefaultErrorBuilder with LexToken {
|
||||||
def tokens = errTokens
|
def tokens = errTokens
|
||||||
}
|
}
|
||||||
def parse(input: String): Result[String, Program] = parser.parse(input)
|
def parse(input: String): Result[String, File => PartialProgram] = parser.parse(input)
|
||||||
private val parser = lexer.fully(`<program>`)
|
private val parser = lexer.fully(`<partial-program>`)
|
||||||
|
|
||||||
|
private type FParsley[A] = Parsley[File => A]
|
||||||
|
|
||||||
|
private def fParsley[A](p: Parsley[A]): FParsley[A] =
|
||||||
|
p map { a => file => a }
|
||||||
|
|
||||||
|
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 fMap[A, F[_]: Functor](p: Parsley[F[File => A]]): FParsley[F[A]] =
|
||||||
|
p map { funcs => file => funcs.map(_(file)) }
|
||||||
|
|
||||||
// Expressions
|
// Expressions
|
||||||
private lazy val `<expr>`: Parsley[Expr] = precedence {
|
private lazy val `<expr>`: FParsley[Expr] = precedence {
|
||||||
SOps(InfixR)(Or from "||") +:
|
GOps(InfixR)(Or from "||") +:
|
||||||
SOps(InfixR)(And from "&&") +:
|
GOps(InfixR)(And from "&&") +:
|
||||||
SOps(InfixN)(Eq from "==", Neq from "!=") +:
|
SOps(InfixN)(Eq from "==", Neq from "!=") +:
|
||||||
SOps(InfixN)(
|
SOps(InfixN)(
|
||||||
Less from "<",
|
Less from "<",
|
||||||
@@ -83,32 +98,33 @@ object parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Atoms
|
// Atoms
|
||||||
private lazy val `<atom>`: Atoms[Expr6] = Atoms(
|
private lazy val `<atom>`: Atoms[File => Expr6] = Atoms(
|
||||||
IntLiter(integer).label("integer literal"),
|
IntLiter(integer).label("integer literal"),
|
||||||
BoolLiter(("true" as true) | ("false" as false)).label("boolean literal"),
|
BoolLiter(("true" as true) | ("false" as false)).label("boolean literal"),
|
||||||
CharLiter(charLit).label("character literal"),
|
CharLiter(charLit).label("character literal"),
|
||||||
StrLiter(stringLit).label("string literal"),
|
`<str-liter>`.label("string literal"),
|
||||||
PairLiter from "null",
|
PairLiter from "null",
|
||||||
`<ident-or-array-elem>`,
|
`<ident-or-array-elem>`,
|
||||||
Parens("(" ~> `<expr>` <~ ")")
|
Parens("(" ~> `<expr>` <~ ")")
|
||||||
)
|
)
|
||||||
private val `<ident>` =
|
private lazy val `<str-liter>` = StrLiter(stringLit)
|
||||||
|
private lazy val `<ident>` =
|
||||||
Ident(ident) | some("*" | "&").verifiedExplain("pointer operators are not allowed")
|
Ident(ident) | some("*" | "&").verifiedExplain("pointer operators are not allowed")
|
||||||
private lazy val `<ident-or-array-elem>` =
|
private lazy val `<ident-or-array-elem>` =
|
||||||
(`<ident>` <~ ("(".verifiedExplain(
|
(`<ident>` <~ ("(".verifiedExplain(
|
||||||
"functions can only be called using 'call' keyword"
|
"functions can only be called using 'call' keyword"
|
||||||
) | unit)) <**> (`<array-indices>` </> identity)
|
) | unit)) <**> (`<array-indices>` </> identity)
|
||||||
private val `<array-indices>` = ArrayElem(some("[" ~> `<expr>` <~ "]"))
|
private lazy val `<array-indices>` = ArrayElem(fMap(some("[" ~> `<expr>` <~ "]")))
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
private lazy val `<type>`: Parsley[Type] =
|
private lazy val `<type>`: FParsley[Type] =
|
||||||
(`<base-type>` | (`<pair-type>` ~> `<pair-elems-type>`)) <**> (`<array-type>` </> identity)
|
(`<base-type>` | (`<pair-type>` ~> `<pair-elems-type>`)) <**> (`<array-type>` </> identity)
|
||||||
private val `<base-type>` =
|
private val `<base-type>` =
|
||||||
(IntType from "int") | (BoolType from "bool") | (CharType from "char") | (StringType from "string")
|
(IntType from "int") | (BoolType from "bool") | (CharType from "char") | (StringType from "string")
|
||||||
private lazy val `<array-type>` =
|
private lazy val `<array-type>` =
|
||||||
ArrayType(countSome("[" ~> "]"))
|
ArrayType(fParsley(countSome("[" ~> "]")))
|
||||||
private val `<pair-type>` = "pair"
|
private val `<pair-type>` = "pair"
|
||||||
private val `<pair-elems-type>`: Parsley[PairType] = PairType(
|
private val `<pair-elems-type>`: FParsley[PairType] = PairType(
|
||||||
"(" ~> `<pair-elem-type>` <~ ",",
|
"(" ~> `<pair-elem-type>` <~ ",",
|
||||||
`<pair-elem-type>` <~ ")"
|
`<pair-elem-type>` <~ ")"
|
||||||
)
|
)
|
||||||
@@ -116,7 +132,7 @@ object parser {
|
|||||||
(`<base-type>` <**> (`<array-type>` </> identity)) |
|
(`<base-type>` <**> (`<array-type>` </> identity)) |
|
||||||
((UntypedPairType from `<pair-type>`) <**>
|
((UntypedPairType from `<pair-type>`) <**>
|
||||||
((`<pair-elems-type>` <**> `<array-type>`)
|
((`<pair-elems-type>` <**> `<array-type>`)
|
||||||
.map(arr => (_: UntypedPairType) => arr) </> identity))
|
.map(arr => (_: File => UntypedPairType) => arr) </> identity))
|
||||||
|
|
||||||
/* Statements
|
/* Statements
|
||||||
Atomic is used in two places here:
|
Atomic is used in two places here:
|
||||||
@@ -127,13 +143,30 @@ object parser {
|
|||||||
invalid syntax check, this only happens at most once per program so this is not a major
|
invalid syntax check, this only happens at most once per program so this is not a major
|
||||||
concern.
|
concern.
|
||||||
*/
|
*/
|
||||||
|
private lazy val `<partial-program>` = PartialProgram(
|
||||||
|
fMap(many(`<import>`)),
|
||||||
|
`<program>`
|
||||||
|
)
|
||||||
|
private lazy val `<import>` = Import(
|
||||||
|
"import" ~> `<import-filename>`,
|
||||||
|
"(" ~> fMap(sepBy1(`<imported-func>`, ",")) <~ ")"
|
||||||
|
)
|
||||||
|
private lazy val `<import-filename>` = `<str-liter>`.label("import file name")
|
||||||
|
private lazy val `<imported-func>` = ImportedFunc(
|
||||||
|
`<ident>`.label("imported function name"),
|
||||||
|
fMap(option("as" ~> `<ident>`)).label("imported function alias")
|
||||||
|
)
|
||||||
private lazy val `<program>` = Program(
|
private lazy val `<program>` = Program(
|
||||||
"begin" ~> (
|
"begin" ~> (
|
||||||
many(
|
fMap(
|
||||||
atomic(
|
many(
|
||||||
`<type>`.label("function declaration") <~> `<ident>` <~ "("
|
fPair(
|
||||||
) <**> `<partial-func-decl>`
|
atomic(
|
||||||
).label("function declaration") |
|
`<type>`.label("function declaration") <~> `<ident>` <~ "("
|
||||||
|
)
|
||||||
|
) <**> `<partial-func-decl>`
|
||||||
|
).label("function declaration")
|
||||||
|
) |
|
||||||
atomic(`<ident>` <~ "(").verifiedExplain("function declaration is missing return type")
|
atomic(`<ident>` <~ "(").verifiedExplain("function declaration is missing return type")
|
||||||
),
|
),
|
||||||
`<stmt>`.label(
|
`<stmt>`.label(
|
||||||
@@ -142,17 +175,23 @@ object parser {
|
|||||||
)
|
)
|
||||||
private lazy val `<partial-func-decl>` =
|
private lazy val `<partial-func-decl>` =
|
||||||
FuncDecl(
|
FuncDecl(
|
||||||
sepBy(`<param>`, ",") <~ ")" <~ "is",
|
fPair(
|
||||||
`<stmt>`.guardAgainst {
|
(fMap(sepBy(`<param>`, ",")) <~ ")" <~ "is") <~>
|
||||||
case stmts if !stmts.isReturning => Seq("all functions must end in a returning statement")
|
(`<stmt>`.guardAgainst {
|
||||||
} <~ "end"
|
// 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>` = Param(`<type>`, `<ident>`)
|
private lazy val `<param>` = Param(`<type>`, `<ident>`)
|
||||||
private lazy val `<stmt>`: Parsley[NonEmptyList[Stmt]] =
|
private lazy val `<stmt>`: FParsley[NonEmptyList[Stmt]] =
|
||||||
(
|
fMap(
|
||||||
`<basic-stmt>`.label("main program body"),
|
(
|
||||||
(many(";" ~> `<basic-stmt>`.label("statement after ';'"))) </> Nil
|
`<basic-stmt>`.label("main program body"),
|
||||||
).zipped(NonEmptyList.apply)
|
(many(";" ~> `<basic-stmt>`.label("statement after ';'"))) </> Nil
|
||||||
|
).zipped(NonEmptyList.apply)
|
||||||
|
)
|
||||||
|
|
||||||
private lazy val `<basic-stmt>` =
|
private lazy val `<basic-stmt>` =
|
||||||
(Skip from "skip")
|
(Skip from "skip")
|
||||||
@@ -160,8 +199,8 @@ object parser {
|
|||||||
| Free("free" ~> `<expr>`.labelAndExplain(LabelType.Expr))
|
| Free("free" ~> `<expr>`.labelAndExplain(LabelType.Expr))
|
||||||
| Return("return" ~> `<expr>`.labelAndExplain(LabelType.Expr))
|
| Return("return" ~> `<expr>`.labelAndExplain(LabelType.Expr))
|
||||||
| Exit("exit" ~> `<expr>`.labelAndExplain(LabelType.Expr))
|
| Exit("exit" ~> `<expr>`.labelAndExplain(LabelType.Expr))
|
||||||
| Print("print" ~> `<expr>`.labelAndExplain(LabelType.Expr), pure(false))
|
| Print("print" ~> `<expr>`.labelAndExplain(LabelType.Expr), fParsley(pure(false)))
|
||||||
| Print("println" ~> `<expr>`.labelAndExplain(LabelType.Expr), pure(true))
|
| Print("println" ~> `<expr>`.labelAndExplain(LabelType.Expr), fParsley(pure(true)))
|
||||||
| If(
|
| If(
|
||||||
"if" ~> `<expr>`.labelWithType(LabelType.Expr) <~ "then",
|
"if" ~> `<expr>`.labelWithType(LabelType.Expr) <~ "then",
|
||||||
`<stmt>` <~ "else",
|
`<stmt>` <~ "else",
|
||||||
@@ -185,9 +224,9 @@ object parser {
|
|||||||
("call" ~> `<ident>`).verifiedExplain(
|
("call" ~> `<ident>`).verifiedExplain(
|
||||||
"function calls' results must be assigned to a variable"
|
"function calls' results must be assigned to a variable"
|
||||||
)
|
)
|
||||||
private lazy val `<lvalue>`: Parsley[LValue] =
|
private lazy val `<lvalue>`: FParsley[LValue] =
|
||||||
`<pair-elem>` | `<ident-or-array-elem>`
|
`<pair-elem>` | `<ident-or-array-elem>`
|
||||||
private lazy val `<rvalue>`: Parsley[RValue] =
|
private lazy val `<rvalue>`: FParsley[RValue] =
|
||||||
`<array-liter>` |
|
`<array-liter>` |
|
||||||
NewPair(
|
NewPair(
|
||||||
"newpair" ~> "(" ~> `<expr>` <~ ",",
|
"newpair" ~> "(" ~> `<expr>` <~ ",",
|
||||||
@@ -196,13 +235,13 @@ object parser {
|
|||||||
`<pair-elem>` |
|
`<pair-elem>` |
|
||||||
Call(
|
Call(
|
||||||
"call" ~> `<ident>` <~ "(",
|
"call" ~> `<ident>` <~ "(",
|
||||||
sepBy(`<expr>`, ",") <~ ")"
|
fMap(sepBy(`<expr>`, ",")) <~ ")"
|
||||||
) | `<expr>`.labelWithType(LabelType.Expr)
|
) | `<expr>`.labelWithType(LabelType.Expr)
|
||||||
private lazy val `<pair-elem>` =
|
private lazy val `<pair-elem>` =
|
||||||
Fst("fst" ~> `<lvalue>`.label("valid pair"))
|
Fst("fst" ~> `<lvalue>`.label("valid pair"))
|
||||||
| Snd("snd" ~> `<lvalue>`.label("valid pair"))
|
| Snd("snd" ~> `<lvalue>`.label("valid pair"))
|
||||||
private lazy val `<array-liter>` = ArrayLiter(
|
private lazy val `<array-liter>` = ArrayLiter(
|
||||||
"[" ~> sepBy(`<expr>`, ",") <~ "]"
|
"[" ~> fMap(sepBy(`<expr>`, ",")) <~ "]"
|
||||||
)
|
)
|
||||||
|
|
||||||
extension (stmts: NonEmptyList[Stmt]) {
|
extension (stmts: NonEmptyList[Stmt]) {
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
package wacc
|
package wacc
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
import cats.effect.IO
|
||||||
|
import cats.syntax.all._
|
||||||
|
import cats.implicits._
|
||||||
|
import cats.data.Chain
|
||||||
|
import cats.data.NonEmptyList
|
||||||
|
import parsley.{Failure, Success}
|
||||||
|
|
||||||
|
private val MAIN = "$main"
|
||||||
|
|
||||||
object renamer {
|
object renamer {
|
||||||
import ast._
|
import ast._
|
||||||
@@ -11,116 +20,271 @@ object renamer {
|
|||||||
case Var
|
case Var
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private case class ScopeKey(path: String, name: String, identType: IdentType)
|
||||||
|
private case class ScopeValue(id: Ident, public: Boolean)
|
||||||
|
|
||||||
private class Scope(
|
private class Scope(
|
||||||
val current: mutable.Map[(String, IdentType), Ident],
|
private val current: mutable.Map[ScopeKey, ScopeValue],
|
||||||
val parent: Map[(String, IdentType), Ident]
|
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.
|
/** Create a new scope with the current scope as its parent.
|
||||||
|
*
|
||||||
|
* To be used for single-threaded applications.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* A new scope with an empty current scope, and this scope flattened into the parent scope.
|
* A new scope with an empty current scope, and this scope flattened into the parent scope.
|
||||||
*/
|
*/
|
||||||
def subscope: Scope =
|
def withSubscope[T](f: Scope => T): T = {
|
||||||
Scope(mutable.Map.empty, Map.empty.withDefault(current.withDefault(parent)))
|
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
|
/** 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.
|
* 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
|
* @param name
|
||||||
* The name of the identifier.
|
* The name of the identifier.
|
||||||
* @param globalNames
|
* @return
|
||||||
* The global map of identifiers to semantic types - the identifier will be added to this
|
* An error, if one occurred.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
def add(ty: SemType | FuncType, name: Ident)(using
|
def add(name: Ident, public: Boolean = false): Chain[Error] = {
|
||||||
globalNames: mutable.Map[Ident, SemType],
|
verifyMutable()
|
||||||
globalFuncs: mutable.Map[Ident, FuncType],
|
val path = name.pos.file.getCanonicalPath
|
||||||
globalNumbering: mutable.Map[String, Int],
|
val identType = name.ty match {
|
||||||
errors: mutable.Builder[Error, List[Error]]
|
|
||||||
) = {
|
|
||||||
val identType = ty match {
|
|
||||||
case _: SemType => IdentType.Var
|
case _: SemType => IdentType.Var
|
||||||
case _: FuncType => IdentType.Func
|
case _: FuncType => IdentType.Func
|
||||||
}
|
}
|
||||||
current.get((name.v, identType)) match {
|
val key = ScopeKey(path, name.v, identType)
|
||||||
case Some(Ident(_, uid)) =>
|
current.get(key) match {
|
||||||
errors += Error.DuplicateDeclaration(name)
|
case Some(ScopeValue(Ident(_, id, _), _)) =>
|
||||||
name.uid = uid
|
name.guid = id
|
||||||
|
Chain.one(Error.DuplicateDeclaration(name))
|
||||||
case None =>
|
case None =>
|
||||||
val uid = globalNumbering.getOrElse(name.v, 0)
|
name.guid = nextGuid()
|
||||||
name.uid = uid
|
current(key) = ScopeValue(name, public)
|
||||||
current((name.v, identType)) = name
|
Chain.empty
|
||||||
|
|
||||||
ty match {
|
|
||||||
case semType: SemType =>
|
|
||||||
globalNames(name) = semType
|
|
||||||
case funcType: FuncType =>
|
|
||||||
globalFuncs(name) = funcType
|
|
||||||
}
|
|
||||||
globalNumbering(name.v) = uid + 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// 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.
|
// Neither is there a way to check whether a default exists, so we have to use a try-catch.
|
||||||
try {
|
try {
|
||||||
Some(current.withDefault(parent)((name, identType)))
|
Some(current.withDefault(parent)(ScopeKey(path, name, identType)))
|
||||||
} catch {
|
} catch {
|
||||||
case _: NoSuchElementException => None
|
case _: NoSuchElementException => None
|
||||||
}
|
}
|
||||||
|
|
||||||
def getVar(name: String): Option[Ident] = get(name, IdentType.Var)
|
def getVar(name: Ident): Option[Ident] =
|
||||||
def getFunc(name: String): Option[Ident] = get(name, IdentType.Func)
|
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
|
private def prepareGlobalScope(
|
||||||
* all identifiers.
|
partialProg: PartialProgram
|
||||||
*
|
)(using scope: Scope): IO[(FuncDecl, Chain[FuncDecl], Chain[Error])] = {
|
||||||
* @param prog
|
def readImportFile(file: File): IO[String] =
|
||||||
* AST of the program
|
IO.blocking(os.read(os.Path(file.getCanonicalPath)))
|
||||||
* @param errors
|
|
||||||
* List of errors to append to
|
def prepareImport(contents: String, file: File)(using
|
||||||
* @return
|
scope: Scope
|
||||||
* Map of all (renamed) identifies to their semantic types
|
): IO[(Chain[FuncDecl], Chain[Error])] = {
|
||||||
*/
|
parser.parse(contents) match {
|
||||||
def rename(prog: Program)(using
|
case Failure(msg) =>
|
||||||
errors: mutable.Builder[Error, List[Error]]
|
IO.pure(Chain.empty, Chain.one(Error.SyntaxError(file, msg)))
|
||||||
): (Map[Ident, SemType], Map[Ident, FuncType]) = {
|
case Success(fn) =>
|
||||||
given globalNames: mutable.Map[Ident, SemType] = mutable.Map.empty
|
val partialProg = fn(file)
|
||||||
given globalFuncs: mutable.Map[Ident, FuncType] = mutable.Map.empty
|
for {
|
||||||
given globalNumbering: mutable.Map[String, Int] = mutable.Map.empty
|
(main, chunks, errors) <- prepareGlobalScope(partialProg)
|
||||||
val scope = Scope(mutable.Map.empty, Map.empty)
|
} 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 Program(funcs, main) = prog
|
||||||
funcs
|
val (funcChunks, funcErrors) = funcs.foldLeft((Chain.empty[FuncDecl], Chain.empty[Error])) {
|
||||||
// First add all function declarations to the scope
|
case ((chunks, errors), func @ FuncDecl(retType, name, params, body)) =>
|
||||||
.map { case FuncDecl(retType, name, params, body) =>
|
|
||||||
val paramTypes = params.map { param =>
|
val paramTypes = params.map { param =>
|
||||||
val paramType = SemType(param.paramType)
|
val paramType = SemType(param.paramType)
|
||||||
|
param.name.ty = paramType
|
||||||
paramType
|
paramType
|
||||||
}
|
}
|
||||||
scope.add(FuncType(SemType(retType), paramTypes), name)
|
name.ty = FuncType(SemType(retType), paramTypes)
|
||||||
(params zip paramTypes, body)
|
(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)
|
||||||
|
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 = addImportsToScope(importFile, funcs)
|
||||||
|
} yield (chunks ++ importChunks, errors ++ importErrors)
|
||||||
}
|
}
|
||||||
// Only then rename the function bodies
|
}
|
||||||
// (functions can call one-another regardless of order of declaration)
|
|
||||||
.foreach { case (params, body) =>
|
for {
|
||||||
val functionScope = scope.subscope
|
(importChunks, importErrors) <- preparedImports
|
||||||
params.foreach { case (param, paramType) =>
|
allChunks = importChunks ++ funcChunks
|
||||||
functionScope.add(paramType, param.name)
|
allErrors = importErrors ++ funcErrors ++ mainBodyErrors
|
||||||
}
|
} yield (mainBodyChunk, allChunks, allErrors)
|
||||||
body.toList.foreach(rename(functionScope.subscope)) // body can shadow function params
|
}
|
||||||
}
|
|
||||||
main.toList.foreach(rename(scope))
|
/** Check scoping of all variables and flatten a program. Also generates semantic types and parses
|
||||||
(globalNames.toMap, globalFuncs.toMap)
|
* any imported files.
|
||||||
|
*
|
||||||
|
* @param partialProg
|
||||||
|
* AST of the program
|
||||||
|
* @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, globalErrors) <- prepareGlobalScope(partialProg)
|
||||||
|
toRename = (main +: chunks).toList
|
||||||
|
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.
|
/** Check scoping of all identifies in a given AST node.
|
||||||
@@ -129,91 +293,90 @@ object renamer {
|
|||||||
* The current scope and flattened parent scope.
|
* The current scope and flattened parent scope.
|
||||||
* @param node
|
* @param node
|
||||||
* The AST 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)(
|
private def rename(scope: Scope)(node: Ident | Stmt | LValue | RValue | Expr): Chain[Error] =
|
||||||
node: Ident | Stmt | LValue | RValue | Expr
|
node match {
|
||||||
)(using
|
// These cases are more interes/globting because the involve making subscopes
|
||||||
globalNames: mutable.Map[Ident, SemType],
|
// or modifying the current scope.
|
||||||
globalFuncs: mutable.Map[Ident, FuncType],
|
case VarDecl(synType, name, value) => {
|
||||||
globalNumbering: mutable.Map[String, Int],
|
// Order matters here. Variable isn't declared until after the value is evaluated.
|
||||||
errors: mutable.Builder[Error, List[Error]]
|
val errors = rename(scope)(value)
|
||||||
): Unit = node match {
|
// Attempt to add the new variable to the current scope.
|
||||||
// These cases are more interesting because the involve making subscopes
|
name.ty = SemType(synType)
|
||||||
// or modifying the current scope.
|
errors ++ scope.add(name)
|
||||||
case VarDecl(synType, name, value) => {
|
}
|
||||||
// Order matters here. Variable isn't declared until after the value is evaluated.
|
case If(cond, thenStmt, elseStmt) => {
|
||||||
rename(scope)(value)
|
val condErrors = rename(scope)(cond)
|
||||||
// Attempt to add the new variable to the current scope.
|
// then and else both have their own scopes
|
||||||
scope.add(SemType(synType), name)
|
val thenErrors = scope.withSubscope(s => thenStmt.foldMap(rename(s)))
|
||||||
}
|
val elseErrors = scope.withSubscope(s => elseStmt.foldMap(rename(s)))
|
||||||
case If(cond, thenStmt, elseStmt) => {
|
condErrors ++ thenErrors ++ elseErrors
|
||||||
rename(scope)(cond)
|
}
|
||||||
// then and else both have their own scopes
|
case While(cond, body) => {
|
||||||
thenStmt.toList.foreach(rename(scope.subscope))
|
val condErrors = rename(scope)(cond)
|
||||||
elseStmt.toList.foreach(rename(scope.subscope))
|
// while bodies have their own scopes
|
||||||
}
|
val bodyErrors = scope.withSubscope(s => body.foldMap(rename(s)))
|
||||||
case While(cond, body) => {
|
condErrors ++ bodyErrors
|
||||||
rename(scope)(cond)
|
}
|
||||||
// while bodies have their own scopes
|
// begin-end blocks have their own scopes
|
||||||
body.toList.foreach(rename(scope.subscope))
|
case Block(body) => scope.withSubscope(s => body.foldMap(rename(s)))
|
||||||
}
|
|
||||||
// begin-end blocks have their own scopes
|
|
||||||
case Block(body) => body.toList.foreach(rename(scope.subscope))
|
|
||||||
|
|
||||||
// These cases are simpler, mostly just recursive calls to rename()
|
// These cases are simpler, mostly just recursive calls to rename()
|
||||||
case Assign(lhs, value) => {
|
case Assign(lhs, value) => {
|
||||||
// Variables may be reassigned with their value in the rhs, so order doesn't matter here.
|
// Variables may be reassigned with their value in the rhs, so order doesn't matter here.
|
||||||
rename(scope)(lhs)
|
rename(scope)(lhs) ++ rename(scope)(value)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
args.foreach(rename(scope))
|
case Read(lhs) => rename(scope)(lhs)
|
||||||
}
|
case Free(expr) => rename(scope)(expr)
|
||||||
case Fst(elem) => rename(scope)(elem)
|
case Return(expr) => rename(scope)(expr)
|
||||||
case Snd(elem) => rename(scope)(elem)
|
case Exit(expr) => rename(scope)(expr)
|
||||||
case ArrayLiter(elems) => elems.foreach(rename(scope))
|
case Print(expr, _) => rename(scope)(expr)
|
||||||
case ArrayElem(name, indices) => {
|
case NewPair(fst, snd) => {
|
||||||
rename(scope)(name)
|
rename(scope)(fst) ++ rename(scope)(snd)
|
||||||
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 Call(name, args) => {
|
||||||
|
val nameErrors = scope.getFunc(name) match {
|
||||||
|
case Some(Ident(realName, guid, ty)) =>
|
||||||
|
name.v = realName
|
||||||
|
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() => ()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,8 @@ object typeChecker {
|
|||||||
import wacc.types._
|
import wacc.types._
|
||||||
|
|
||||||
case class TypeCheckerCtx(
|
case class TypeCheckerCtx(
|
||||||
globalNames: Map[ast.Ident, SemType],
|
|
||||||
globalFuncs: Map[ast.Ident, FuncType],
|
|
||||||
errors: mutable.Builder[Error, List[Error]]
|
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 =
|
def error(err: Error): SemType =
|
||||||
errors += err
|
errors += err
|
||||||
?
|
?
|
||||||
@@ -99,18 +94,17 @@ object typeChecker {
|
|||||||
* The type checker context which includes the global names and functions, and an errors
|
* The type checker context which includes the global names and functions, and an errors
|
||||||
* builder.
|
* builder.
|
||||||
*/
|
*/
|
||||||
def check(prog: ast.Program)(using
|
def check(prog: ast.Program, errors: mutable.Builder[Error, List[Error]]): microWacc.Program =
|
||||||
ctx: TypeCheckerCtx
|
given ctx: TypeCheckerCtx = TypeCheckerCtx(errors)
|
||||||
): microWacc.Program =
|
|
||||||
microWacc.Program(
|
microWacc.Program(
|
||||||
// Ignore function syntax types for return value and params, since those have been converted
|
// Ignore function syntax types for return value and params, since those have been converted
|
||||||
// to SemTypes by the renamer.
|
// to SemTypes by the renamer.
|
||||||
prog.funcs.map { case ast.FuncDecl(_, name, params, stmts) =>
|
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.FuncDecl(
|
||||||
microWacc.Ident(name.v, name.uid)(retType),
|
microWacc.Ident(name.v, name.guid)(retType),
|
||||||
params.zip(paramTypes).map { case (ast.Param(_, ident), ty) =>
|
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
|
stmts.toList
|
||||||
.flatMap(
|
.flatMap(
|
||||||
@@ -134,15 +128,20 @@ object typeChecker {
|
|||||||
): List[microWacc.Stmt] = stmt match {
|
): List[microWacc.Stmt] = stmt match {
|
||||||
// Ignore the type of the variable, since it has been converted to a SemType by the renamer.
|
// Ignore the type of the variable, since it has been converted to a SemType by the renamer.
|
||||||
case ast.VarDecl(_, name, value) =>
|
case ast.VarDecl(_, name, value) =>
|
||||||
val expectedTy = ctx.typeOf(name)
|
val expectedTy = name.ty
|
||||||
val typedValue = checkValue(
|
val typedValue = checkValue(
|
||||||
value,
|
value,
|
||||||
Constraint.Is(
|
Constraint.Is(
|
||||||
expectedTy,
|
expectedTy.asInstanceOf[SemType],
|
||||||
s"variable ${name.v} must be assigned a value of type $expectedTy"
|
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) =>
|
case ast.Assign(lhs, rhs) =>
|
||||||
val lhsTyped = checkLValue(lhs, Constraint.Unconstrained)
|
val lhsTyped = checkLValue(lhs, Constraint.Unconstrained)
|
||||||
val rhsTyped =
|
val rhsTyped =
|
||||||
@@ -315,7 +314,7 @@ object typeChecker {
|
|||||||
KnownType.Pair(fstTyped.ty, sndTyped.ty).satisfies(constraint, l.pos)
|
KnownType.Pair(fstTyped.ty, sndTyped.ty).satisfies(constraint, l.pos)
|
||||||
)
|
)
|
||||||
case ast.Call(id, args) =>
|
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) {
|
if (args.length != paramTys.length) {
|
||||||
ctx.error(Error.FunctionParamsMismatch(id, paramTys.length, args.length, funcTy))
|
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) =>
|
val argsTyped = args.zip(paramTys).map { case (arg, paramTy) =>
|
||||||
checkValue(arg, Constraint.Is(paramTy, s"argument type mismatch in function ${id.v}"))
|
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
|
// Unary operators
|
||||||
case ast.Negate(x) =>
|
case ast.Negate(x) =>
|
||||||
@@ -416,30 +415,32 @@ object typeChecker {
|
|||||||
private def checkLValue(value: ast.LValue, constraint: Constraint)(using
|
private def checkLValue(value: ast.LValue, constraint: Constraint)(using
|
||||||
ctx: TypeCheckerCtx
|
ctx: TypeCheckerCtx
|
||||||
): microWacc.LValue = value match {
|
): microWacc.LValue = value match {
|
||||||
case id @ ast.Ident(name, uid) =>
|
case id @ ast.Ident(name, guid, ty) =>
|
||||||
microWacc.Ident(name, uid)(ctx.typeOf(id).satisfies(constraint, id.pos))
|
microWacc.Ident(name, guid)(ty.asInstanceOf[SemType].satisfies(constraint, id.pos))
|
||||||
case ast.ArrayElem(id, indices) =>
|
case ast.ArrayElem(id, indices) =>
|
||||||
val arrayTy = ctx.typeOf(id)
|
val arrayTy = id.ty.asInstanceOf[SemType]
|
||||||
val (elemTy, indicesTyped) = indices.mapAccumulate(arrayTy) { (acc, elem) =>
|
val (elemTy, indicesTyped) = indices.mapAccumulate(arrayTy.asInstanceOf[SemType]) {
|
||||||
val idxTyped = checkValue(elem, Constraint.Is(KnownType.Int, "array index must be an int"))
|
(acc, elem) =>
|
||||||
val next = acc match {
|
val idxTyped =
|
||||||
case KnownType.Array(innerTy) => innerTy
|
checkValue(elem, Constraint.Is(KnownType.Int, "array index must be an int"))
|
||||||
case ? => ? // we can keep indexing an unknown type
|
val next = acc match {
|
||||||
case nonArrayTy =>
|
case KnownType.Array(innerTy) => innerTy
|
||||||
ctx.error(
|
case ? => ? // we can keep indexing an unknown type
|
||||||
Error.TypeMismatch(
|
case nonArrayTy =>
|
||||||
elem.pos,
|
ctx.error(
|
||||||
KnownType.Array(?),
|
Error.TypeMismatch(
|
||||||
acc,
|
elem.pos,
|
||||||
"cannot index into a non-array"
|
KnownType.Array(?),
|
||||||
|
acc,
|
||||||
|
"cannot index into a non-array"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
?
|
||||||
?
|
}
|
||||||
}
|
(next, idxTyped)
|
||||||
(next, idxTyped)
|
|
||||||
}
|
}
|
||||||
val firstArrayElem = microWacc.ArrayElem(
|
val firstArrayElem = microWacc.ArrayElem(
|
||||||
microWacc.Ident(id.v, id.uid)(arrayTy),
|
microWacc.Ident(id.v, id.guid)(arrayTy),
|
||||||
indicesTyped.head
|
indicesTyped.head
|
||||||
)(elemTy.satisfies(constraint, value.pos))
|
)(elemTy.satisfies(constraint, value.pos))
|
||||||
val arrayElem = indicesTyped.tail.foldLeft(firstArrayElem) { (acc, idx) =>
|
val arrayElem = indicesTyped.tail.foldLeft(firstArrayElem) { (acc, idx) =>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package wacc
|
|||||||
object types {
|
object types {
|
||||||
import ast._
|
import ast._
|
||||||
|
|
||||||
sealed trait SemType {
|
sealed trait RenamerType
|
||||||
|
|
||||||
|
sealed trait SemType extends RenamerType {
|
||||||
override def toString(): String = this match {
|
override def toString(): String = this match {
|
||||||
case KnownType.Int => "int"
|
case KnownType.Int => "int"
|
||||||
case KnownType.Bool => "bool"
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package wacc
|
package wacc
|
||||||
|
|
||||||
import org.scalatest.BeforeAndAfterAll
|
import org.scalatest.BeforeAndAfterAll
|
||||||
import org.scalatest.flatspec.AnyFlatSpec
|
|
||||||
import org.scalatest.Inspectors.forEvery
|
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 java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
import sys.process._
|
import sys.process._
|
||||||
import java.io.PrintStream
|
|
||||||
import scala.io.Source
|
import scala.io.Source
|
||||||
|
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 =
|
val files =
|
||||||
allWaccFiles("wacc-examples/valid").map { p =>
|
allWaccFiles("wacc-examples/valid").map { p =>
|
||||||
(p.toString, List(0))
|
(p.toString, List(0))
|
||||||
@@ -21,97 +26,119 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll {
|
|||||||
} ++
|
} ++
|
||||||
allWaccFiles("wacc-examples/invalid/whack").map { p =>
|
allWaccFiles("wacc-examples/invalid/whack").map { p =>
|
||||||
(p.toString, List(100, 200))
|
(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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// tests go here
|
|
||||||
forEvery(files) { (filename, expectedResult) =>
|
forEvery(files) { (filename, expectedResult) =>
|
||||||
val baseFilename = filename.stripSuffix(".wacc")
|
val baseFilename = filename.stripSuffix(".wacc")
|
||||||
given stdout: PrintStream = PrintStream(File(baseFilename + ".out"))
|
|
||||||
|
|
||||||
s"$filename" should "be compiled with correct result" in {
|
s"$filename" - {
|
||||||
val result = compile(filename)
|
"should be compiled with correct result" in {
|
||||||
assert(expectedResult.contains(result))
|
if (fileIsPendingFrontend(filename))
|
||||||
}
|
IO.pure(pending)
|
||||||
|
|
||||||
if (expectedResult == List(0)) it should "run with correct result" in {
|
|
||||||
if (fileIsDisallowedBackend(filename)) pending
|
|
||||||
|
|
||||||
// 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
|
else
|
||||||
contents
|
compileWacc(Path.of(filename), outputDir = None, log = false).map { result =>
|
||||||
.drop(outputLineIdx + 1)
|
expectedResult should contain(result)
|
||||||
.takeWhile(_.startsWith("#"))
|
}
|
||||||
.map(_.stripPrefix("#").stripLeading)
|
}
|
||||||
.mkString("\n")
|
|
||||||
|
|
||||||
val exitLineIdx = contents.indexWhere(_.matches("^# ?[Ee]xit:.*$"))
|
if (expectedResult == List(0)) {
|
||||||
val expectedExit =
|
"should run with correct result" in {
|
||||||
if (exitLineIdx == -1) 0
|
if (fileIsDisallowedBackend(filename))
|
||||||
else contents(exitLineIdx + 1).stripPrefix("#").strip.toInt
|
IO.pure(succeed)
|
||||||
|
else if (fileIsPendingBackend(filename))
|
||||||
|
IO.pure(pending)
|
||||||
|
else
|
||||||
|
for {
|
||||||
|
contents <- IO(Source.fromFile(File(filename)).getLines.toList)
|
||||||
|
inputLine = extractInput(contents)
|
||||||
|
expectedOutput = extractOutput(contents)
|
||||||
|
expectedExit = extractExit(contents)
|
||||||
|
|
||||||
// Assembly and link using gcc
|
asmFilename = baseFilename + ".s"
|
||||||
val asmFilename = baseFilename + ".s"
|
execFilename = baseFilename
|
||||||
val execFilename = baseFilename
|
gccResult <- IO(s"gcc -o $execFilename -z noexecstack $asmFilename".!)
|
||||||
val gccResult = s"gcc -o $execFilename -z noexecstack $asmFilename".!
|
|
||||||
assert(gccResult == 0)
|
|
||||||
|
|
||||||
// Run the executable with the provided input
|
_ = assert(gccResult == 0)
|
||||||
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 = _ => ()
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(process.exitValue == expectedExit)
|
stdout <- IO.pure(new StringBuilder)
|
||||||
assert(
|
process <- IO {
|
||||||
stdout.toString
|
s"timeout 5s $execFilename" run ProcessIO(
|
||||||
.replaceAll("0x[0-9a-f]+", "#addrs#")
|
in = w => {
|
||||||
.replaceAll("fatal error:.*", "#runtime_error#\u0000")
|
w.write(inputLine.getBytes)
|
||||||
.takeWhile(_ != '\u0000')
|
w.close()
|
||||||
== expectedOutput
|
},
|
||||||
)
|
out = Source.fromInputStream(_).addString(stdout),
|
||||||
|
err = _ => ()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode <- IO.pure(process.exitValue)
|
||||||
|
|
||||||
|
} yield {
|
||||||
|
exitCode shouldBe expectedExit
|
||||||
|
normalizeOutput(stdout.toString) shouldBe expectedOutput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def allWaccFiles(dir: String): IndexedSeq[os.Path] =
|
def allWaccFiles(dir: String): IndexedSeq[os.Path] =
|
||||||
val d = java.io.File(dir)
|
val d = java.io.File(dir)
|
||||||
os.walk(os.Path(d.getAbsolutePath)).filter { _.ext == "wacc" }
|
os.walk(os.Path(d.getAbsolutePath)).filter(_.ext == "wacc")
|
||||||
|
|
||||||
def fileIsDisallowedBackend(filename: String): Boolean =
|
private def fileIsDisallowedBackend(filename: String): Boolean =
|
||||||
Seq(
|
filename.matches("^.*wacc-examples/valid/advanced.*$")
|
||||||
// format: off
|
|
||||||
// disable formatting to avoid binPack
|
private def fileIsPendingFrontend(filename: String): Boolean =
|
||||||
"^.*wacc-examples/valid/advanced.*$",
|
List(
|
||||||
// "^.*wacc-examples/valid/array.*$",
|
// "^.*extension/examples/invalid/syntax/imports/importBadSyntax.*$",
|
||||||
// "^.*wacc-examples/valid/basic/exit.*$",
|
// "^.*extension/examples/invalid/semantics/imports.*$",
|
||||||
// "^.*wacc-examples/valid/basic/skip.*$",
|
// "^.*extension/examples/valid/imports.*$"
|
||||||
// "^.*wacc-examples/valid/expressions.*$",
|
).exists(filename.matches)
|
||||||
// "^.*wacc-examples/valid/function/nested_functions.*$",
|
|
||||||
// "^.*wacc-examples/valid/function/simple_functions.*$",
|
private def fileIsPendingBackend(filename: String): Boolean =
|
||||||
// "^.*wacc-examples/valid/if.*$",
|
List(
|
||||||
// "^.*wacc-examples/valid/IO/print.*$",
|
// "^.*extension/examples/invalid/syntax/imports.*$",
|
||||||
// "^.*wacc-examples/valid/IO/read.*$",
|
// "^.*extension/examples/invalid/semantics/imports.*$",
|
||||||
// "^.*wacc-examples/valid/IO/IOLoop.wacc.*$",
|
// "^.*extension/examples/valid/imports.*$"
|
||||||
// "^.*wacc-examples/valid/IO/IOSequence.wacc.*$",
|
).exists(filename.matches)
|
||||||
// "^.*wacc-examples/valid/pairs.*$",
|
|
||||||
//"^.*wacc-examples/valid/runtimeErr.*$",
|
private def extractInput(contents: List[String]): String =
|
||||||
// "^.*wacc-examples/valid/scope.*$",
|
contents
|
||||||
// "^.*wacc-examples/valid/sequence.*$",
|
.find(_.matches("^# ?[Ii]nput:.*$"))
|
||||||
// "^.*wacc-examples/valid/variables.*$",
|
.map(_.split(":").last.strip + "\n")
|
||||||
// "^.*wacc-examples/valid/while.*$",
|
.getOrElse("")
|
||||||
// format: on
|
|
||||||
).find(filename.matches).isDefined
|
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')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user