From eb7387b49ce47383b0f23c7589adbec97a82a931 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 21 Feb 2025 18:18:35 +0000 Subject: [PATCH 1/7] chore: update dependencies parsley-cats and os-lib --- project.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project.scala b/project.scala index e6fb151..4deaa08 100644 --- a/project.scala +++ b/project.scala @@ -3,8 +3,8 @@ // dependencies //> using dep com.github.j-mie6::parsley::5.0.0-M10 -//> using dep com.github.j-mie6::parsley-cats::1.3.0 -//> using dep com.lihaoyi::os-lib::0.11.3 +//> using dep com.github.j-mie6::parsley-cats::1.5.0 +//> using dep com.lihaoyi::os-lib::0.11.4 //> using dep com.github.scopt::scopt::4.1.0 //> using test.dep org.scalatest::scalatest::3.2.19 From 87691902be7493a35b37f36633539b7f8d205013 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 21 Feb 2025 18:18:44 +0000 Subject: [PATCH 2/7] feat: set wacc target to x86-64 --- wacc.target | 1 + 1 file changed, 1 insertion(+) create mode 100644 wacc.target diff --git a/wacc.target b/wacc.target new file mode 100644 index 0000000..83726ed --- /dev/null +++ b/wacc.target @@ -0,0 +1 @@ +x86-64 From 39c695b1bbde789d17ffcf7c6538d2b210eef90c Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 21 Feb 2025 18:19:03 +0000 Subject: [PATCH 3/7] feat: output backend files --- src/main/wacc/Main.scala | 208 ++++++++++++++++++++++++++--- src/main/wacc/frontend/Error.scala | 29 ++-- 2 files changed, 205 insertions(+), 32 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 445d9c1..8221440 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -4,6 +4,10 @@ import scala.collection.mutable import parsley.{Failure, Success} import scopt.OParser import java.io.File +import java.io.PrintStream + +import assemblyIR as asm +import wacc.microWacc.IntLiter case class CliConfig( file: File = new File(".") @@ -30,7 +34,9 @@ val cliParser = { ) } -def compile(contents: String): Int = { +def frontend( + contents: String +)(using stdout: PrintStream): Either[microWacc.Program, Int] = { parser.parse(contents) match { case Success(prog) => given errors: mutable.Builder[Error, List[Error]] = List.newBuilder @@ -39,28 +45,194 @@ def compile(contents: String): Int = { val typedProg = typeChecker.check(prog) if (errors.result.nonEmpty) { given errorContent: String = contents - errors.result - .map { error => - printError(error) - error match { - case _: Error.InternalError => 201 - case _ => 200 + Right( + errors.result + .map { error => + printError(error) + error match { + case _: Error.InternalError => 201 + case _ => 200 + } } - } - .max() - } else { - println(typedProg) - 0 - } + .max() + ) + } else Left(typedProg) case Failure(msg) => - println(msg) - 100 + stdout.println(msg) + Right(100) } } +val s = "enter an integer to echo" +def backend(typedProg: microWacc.Program): List[asm.AsmLine] | String = + typedProg match { + case microWacc.Program( + Nil, + microWacc.Call(microWacc.Builtin.Exit, microWacc.IntLiter(v) :: Nil) :: Nil + ) => + s""".intel_syntax noprefix +.globl main +main: + mov edi, ${v} + call exit@plt +""" + case microWacc.Program( + Nil, + microWacc.Assign(microWacc.Ident("x", _), microWacc.IntLiter(1)) :: + microWacc.Call(microWacc.Builtin.Println, _) :: + microWacc.Assign( + microWacc.Ident("x", _), + microWacc.Call(microWacc.Builtin.ReadInt, Nil) + ) :: + microWacc.Call(microWacc.Builtin.Println, microWacc.Ident("x", _) :: Nil) :: Nil + ) => + """.intel_syntax noprefix +.globl main +.section .rodata +# length of .L.str0 + .int 24 +.L.str0: + .asciz "enter an integer to echo" +.text +main: + push rbp + # push {rbx, r12} + sub rsp, 16 + mov qword ptr [rsp], rbx + mov qword ptr [rsp + 8], r12 + mov rbp, rsp + mov r12d, 1 + lea rdi, [rip + .L.str0] + # statement primitives do not return results (but will clobber r0/rax) + call _prints + call _println + # load the current value in the destination of the read so it supports defaults + mov edi, r12d + call _readi + mov r12d, eax + mov edi, eax + # statement primitives do not return results (but will clobber r0/rax) + call _printi + call _println + mov rax, 0 + # pop/peek {rbx, r12} + mov rbx, qword ptr [rsp] + mov r12, qword ptr [rsp + 8] + add rsp, 16 + pop rbp + ret + +.section .rodata +# length of .L._printi_str0 + .int 2 +.L._printi_str0: + .asciz "%d" +.text +_printi: + push rbp + mov rbp, rsp + # external calls must be stack-aligned to 16 bytes, accomplished by masking with fffffffffffffff0 + and rsp, -16 + mov esi, edi + lea rdi, [rip + .L._printi_str0] + # on x86, al represents the number of SIMD registers used as variadic arguments + mov al, 0 + call printf@plt + mov rdi, 0 + call fflush@plt + mov rsp, rbp + pop rbp + ret + +.section .rodata +# length of .L._prints_str0 + .int 4 +.L._prints_str0: + .asciz "%.*s" +.text +_prints: + push rbp + mov rbp, rsp + # external calls must be stack-aligned to 16 bytes, accomplished by masking with fffffffffffffff0 + and rsp, -16 + mov rdx, rdi + mov esi, dword ptr [rdi - 4] + lea rdi, [rip + .L._prints_str0] + # on x86, al represents the number of SIMD registers used as variadic arguments + mov al, 0 + call printf@plt + mov rdi, 0 + call fflush@plt + mov rsp, rbp + pop rbp + ret + +.section .rodata +# length of .L._println_str0 + .int 0 +.L._println_str0: + .asciz "" +.text +_println: + push rbp + mov rbp, rsp + # external calls must be stack-aligned to 16 bytes, accomplished by masking with fffffffffffffff0 + and rsp, -16 + lea rdi, [rip + .L._println_str0] + call puts@plt + mov rdi, 0 + call fflush@plt + mov rsp, rbp + pop rbp + ret + +.section .rodata +# length of .L._readi_str0 + .int 2 +.L._readi_str0: + .asciz "%d" +.text +_readi: + push rbp + mov rbp, rsp + # external calls must be stack-aligned to 16 bytes, accomplished by masking with fffffffffffffff0 + and rsp, -16 + # RDI contains the "original" value of the destination of the read + # allocate space on the stack to store the read: preserve alignment! + # the passed default argument should be stored in case of EOF + sub rsp, 16 + mov dword ptr [rsp], edi + lea rsi, qword ptr [rsp] + lea rdi, [rip + .L._readi_str0] + # on x86, al represents the number of SIMD registers used as variadic arguments + mov al, 0 + call scanf@plt + mov eax, dword ptr [rsp] + add rsp, 16 + mov rsp, rbp + pop rbp + ret + """ + case _ => List() + } + +def compile(filename: String)(using stdout: PrintStream = Console.out): Int = + frontend(os.read(os.Path(filename))) match { + case Left(typedProg) => + backend(typedProg) match { + case s: String => + os.write.over(os.Path(filename.stripSuffix(".wacc") + ".s"), s) + case ops: List[asm.AsmLine] => { + val outFile = File(filename.stripSuffix(".wacc") + ".s") + writer.writeTo(ops, PrintStream(outFile)) + } + } + 0 + case Right(exitCode) => exitCode + } + def main(args: Array[String]): Unit = OParser.parse(cliParser, args, CliConfig()) match { - case Some(config) => - System.exit(compile(os.read(os.Path(config.file.getAbsolutePath)))) - case None => + case Some(config) => compile(config.file.getAbsolutePath) + case None => } diff --git a/src/main/wacc/frontend/Error.scala b/src/main/wacc/frontend/Error.scala index 0f3c01d..9c02a60 100644 --- a/src/main/wacc/frontend/Error.scala +++ b/src/main/wacc/frontend/Error.scala @@ -2,6 +2,7 @@ package wacc import wacc.ast.Position import wacc.types._ +import java.io.PrintStream /** Error types for semantic errors */ @@ -23,39 +24,39 @@ enum Error { * @param errorContent * Contents of the file to generate code snippets */ -def printError(error: Error)(using errorContent: String): Unit = { - println("Semantic error:") +def printError(error: Error)(using errorContent: String, stdout: PrintStream): Unit = { + stdout.println("Semantic error:") error match { case Error.DuplicateDeclaration(ident) => printPosition(ident.pos) - println(s"Duplicate declaration of identifier ${ident.v}") + stdout.println(s"Duplicate declaration of identifier ${ident.v}") highlight(ident.pos, ident.v.length) case Error.UndeclaredVariable(ident) => printPosition(ident.pos) - println(s"Undeclared variable ${ident.v}") + stdout.println(s"Undeclared variable ${ident.v}") highlight(ident.pos, ident.v.length) case Error.UndefinedFunction(ident) => printPosition(ident.pos) - println(s"Undefined function ${ident.v}") + stdout.println(s"Undefined function ${ident.v}") highlight(ident.pos, ident.v.length) case Error.FunctionParamsMismatch(id, expected, got, funcType) => printPosition(id.pos) - println(s"Function expects $expected parameters, got $got") - println( + 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) - println(s"Type mismatch: $msg\nExpected: $expected\nGot: $got") + stdout.println(s"Type mismatch: $msg\nExpected: $expected\nGot: $got") highlight(pos, 1) case Error.SemanticError(pos, msg) => printPosition(pos) - println(msg) + stdout.println(msg) highlight(pos, 1) case wacc.Error.InternalError(pos, msg) => printPosition(pos) - println(s"Internal error: $msg") + stdout.println(s"Internal error: $msg") highlight(pos, 1) } @@ -70,7 +71,7 @@ def printError(error: Error)(using errorContent: String): Unit = { * @param errorContent * Contents of the file to generate code snippets */ -def highlight(pos: Position, size: Int)(using errorContent: String): Unit = { +def highlight(pos: Position, size: Int)(using errorContent: String, stdout: PrintStream): Unit = { val lines = errorContent.split("\n") val preLine = if (pos.line > 1) lines(pos.line - 2) else "" @@ -78,7 +79,7 @@ def highlight(pos: Position, size: Int)(using errorContent: String): Unit = { val postLine = if (pos.line < lines.size) lines(pos.line) else "" val linePointer = " " * (pos.column + 2) + ("^" * (size)) + "\n" - println( + stdout.println( s" >$preLine\n >$midLine\n$linePointer >$postLine" ) } @@ -88,6 +89,6 @@ def highlight(pos: Position, size: Int)(using errorContent: String): Unit = { * @param pos * Position of the error */ -def printPosition(pos: Position): Unit = { - println(s"(line ${pos.line}, column ${pos.column}):") +def printPosition(pos: Position)(using stdout: PrintStream): Unit = { + stdout.println(s"(line ${pos.line}, column ${pos.column}):") } From 42ff9c9e790ab2527b382337693b24beb563154a Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 21 Feb 2025 18:19:23 +0000 Subject: [PATCH 4/7] test: backend tests --- src/test/wacc/examples.scala | 122 +++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/src/test/wacc/examples.scala b/src/test/wacc/examples.scala index f62d537..abff693 100644 --- a/src/test/wacc/examples.scala +++ b/src/test/wacc/examples.scala @@ -3,6 +3,9 @@ package wacc import org.scalatest.{ParallelTestExecution, BeforeAndAfterAll} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.Inspectors.forEvery +import java.io.File +import sys.process._ +import java.io.PrintStream class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll with ParallelTestExecution { val files = @@ -20,12 +23,51 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll with Paral } // tests go here - forEvery(files.filter { (filename, _) => - !fileIsDissallowed(filename) - }) { (filename, expectedResult) => - s"$filename" should "be parsed with correct result" in { - val contents = os.read(os.Path(filename)) - assert(expectedResult.contains(compile(contents))) + forEvery(files) { (filename, expectedResult) => + val baseFilename = filename.stripSuffix(".wacc") + given stdout: PrintStream = PrintStream(File(baseFilename + ".out")) + val result = compile(filename) + + s"$filename" should "be compiled with correct result" in { + assert(expectedResult.contains(result)) + } + + if (result == 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).getOrElse("") + val outputLineIdx = contents.indexWhere(_.matches("^# ?[Oo]utput:.*$")) + val expectedOutput = + if (outputLineIdx == -1) "" + else + contents + .drop(outputLineIdx + 1) + .takeWhile(_.startsWith("#")) + .map(_.stripPrefix("#").stripLeading) + .mkString("\n") + + val exitLineIdx = contents.indexWhere(_.matches("^# ?[Ee]xit:.*$")) + val expectedExit = + if (exitLineIdx == -1) 0 + else contents(exitLineIdx + 1).stripPrefix("#").strip.toInt + + // Assembly and link using gcc + val asmFilename = baseFilename + ".s" + val execFilename = baseFilename + val gccResult = s"gcc -o $execFilename -z noexecstack $asmFilename".! + assert(gccResult == 0) + + // Run the executable with the provided input + val stdout = new StringBuilder + // val execResult = s"$execFilename".!(ProcessLogger(stdout.append(_))) + val execResult = + s"echo $inputLine" #| s"timeout 5s $execFilename" ! ProcessLogger(stdout.append(_)) + + assert(execResult == expectedExit) + assert(stdout.toString == expectedOutput) } } @@ -33,57 +75,27 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll with Paral val d = java.io.File(dir) os.walk(os.Path(d.getAbsolutePath)).filter { _.ext == "wacc" } - def fileIsDissallowed(filename: String): Boolean = + def fileIsDisallowedBackend(filename: String): Boolean = Seq( // format: off // disable formatting to avoid binPack - // "wacc-examples/valid/advanced", - // "wacc-examples/valid/array", - // "wacc-examples/valid/basic/exit", - // "wacc-examples/valid/basic/skip", - // "wacc-examples/valid/expressions", - // "wacc-examples/valid/function/nested_functions", - // "wacc-examples/valid/function/simple_functions", - // "wacc-examples/valid/if", - // "wacc-examples/valid/IO/print", - // "wacc-examples/valid/IO/read", - // "wacc-examples/valid/IO/IOLoop.wacc", - // "wacc-examples/valid/IO/IOSequence.wacc", - // "wacc-examples/valid/pairs", - // "wacc-examples/valid/runtimeErr", - // "wacc-examples/valid/scope", - // "wacc-examples/valid/sequence", - // "wacc-examples/valid/variables", - // "wacc-examples/valid/while", - // invalid (syntax) - // "wacc-examples/invalid/syntaxErr/array", - // "wacc-examples/invalid/syntaxErr/basic", - // "wacc-examples/invalid/syntaxErr/expressions", - // "wacc-examples/invalid/syntaxErr/function", - // "wacc-examples/invalid/syntaxErr/if", - // "wacc-examples/invalid/syntaxErr/literals", - // "wacc-examples/invalid/syntaxErr/pairs", - // "wacc-examples/invalid/syntaxErr/print", - // "wacc-examples/invalid/syntaxErr/sequence", - // "wacc-examples/invalid/syntaxErr/variables", - // "wacc-examples/invalid/syntaxErr/while", - // invalid (semantic) - // "wacc-examples/invalid/semanticErr/array", - // "wacc-examples/invalid/semanticErr/exit", - // "wacc-examples/invalid/semanticErr/expressions", - // "wacc-examples/invalid/semanticErr/function", - // "wacc-examples/invalid/semanticErr/if", - // "wacc-examples/invalid/semanticErr/IO", - // "wacc-examples/invalid/semanticErr/multiple", - // "wacc-examples/invalid/semanticErr/pairs", - // "wacc-examples/invalid/semanticErr/print", - // "wacc-examples/invalid/semanticErr/read", - // "wacc-examples/invalid/semanticErr/scope", - // "wacc-examples/invalid/semanticErr/variables", - // "wacc-examples/invalid/semanticErr/while", - // invalid (whack) - // "wacc-examples/invalid/whack" + "^.*wacc-examples/valid/advanced.*$", + "^.*wacc-examples/valid/array.*$", + "^.*wacc-examples/valid/basic/skip.*$", + "^.*wacc-examples/valid/expressions.*$", + "^.*wacc-examples/valid/function/nested_functions.*$", + "^.*wacc-examples/valid/function/simple_functions.*$", + "^.*wacc-examples/valid/if.*$", + "^.*wacc-examples/valid/IO/print.*$", + "^.*wacc-examples/valid/IO/read(?!echoInt\\.wacc).*$", + "^.*wacc-examples/valid/IO/IOLoop.wacc.*$", + "^.*wacc-examples/valid/IO/IOSequence.wacc.*$", + "^.*wacc-examples/valid/pairs.*$", + "^.*wacc-examples/valid/runtimeErr.*$", + "^.*wacc-examples/valid/scope.*$", + "^.*wacc-examples/valid/sequence.*$", + "^.*wacc-examples/valid/variables.*$", + "^.*wacc-examples/valid/while.*$", // format: on - // format: on - ).find(filename.contains).isDefined + ).find(filename.matches).isDefined } From b2da8c2408cb036258528363f8ab41b7587d47da Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 21 Feb 2025 18:30:33 +0000 Subject: [PATCH 5/7] ci: use x86 image for tests --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c9d7b1..98a6044 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,6 +45,7 @@ compile_jvm: - .scala-build/ test_jvm: + image: gumjoe/wacc-ci-scala:x86 stage: test # Use our own runner (not cloud VM or shared) to ensure we have multiple cores. tags: [ large ] From ab28f0950a047a80d976e8cb55f0f66a760ad108 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 21 Feb 2025 18:46:10 +0000 Subject: [PATCH 6/7] fix: main outputs to current dir --- src/main/wacc/Main.scala | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 8221440..637923d 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -216,15 +216,18 @@ _readi: case _ => List() } -def compile(filename: String)(using stdout: PrintStream = Console.out): Int = +def compile(filename: String, outFile: Option[File] = None)(using + stdout: PrintStream = Console.out +): Int = frontend(os.read(os.Path(filename))) match { case Left(typedProg) => + val asmFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s")) + println("OUTPUT TO" + asmFile.getAbsolutePath()) backend(typedProg) match { case s: String => - os.write.over(os.Path(filename.stripSuffix(".wacc") + ".s"), s) + os.write.over(os.Path(asmFile.getAbsolutePath), s) case ops: List[asm.AsmLine] => { - val outFile = File(filename.stripSuffix(".wacc") + ".s") - writer.writeTo(ops, PrintStream(outFile)) + writer.writeTo(ops, PrintStream(asmFile)) } } 0 @@ -233,6 +236,10 @@ def compile(filename: String)(using stdout: PrintStream = Console.out): Int = def main(args: Array[String]): Unit = OParser.parse(cliParser, args, CliConfig()) match { - case Some(config) => compile(config.file.getAbsolutePath) - case None => + case Some(config) => + compile( + config.file.getAbsolutePath, + outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s")) + ) + case None => } From 0391b9deba388b8de809098656fc8123b5c19171 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 21 Feb 2025 18:51:50 +0000 Subject: [PATCH 7/7] fix: remove logging from Main --- src/main/wacc/Main.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/wacc/Main.scala b/src/main/wacc/Main.scala index 637923d..e8e7b7b 100644 --- a/src/main/wacc/Main.scala +++ b/src/main/wacc/Main.scala @@ -222,7 +222,6 @@ def compile(filename: String, outFile: Option[File] = None)(using frontend(os.read(os.Path(filename))) match { case Left(typedProg) => val asmFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s")) - println("OUTPUT TO" + asmFile.getAbsolutePath()) backend(typedProg) match { case s: String => os.write.over(os.Path(asmFile.getAbsolutePath), s)