131 Commits

Author SHA1 Message Date
3b723392a7 fix: do not overwrite RDI in free 2025-02-28 18:47:36 +00:00
9a0d3e38a4 refactor: merge MemLocation with IndexedAddress
Merge request lab2425_spring/WACC_37!37
2025-02-28 18:44:48 +00:00
37812fb5a7 fix: copy whole register on assignment 2025-02-28 18:41:31 +00:00
578a28a222 Revert "fix: zero-out D32 as well"
This reverts commit 0eaf2186b6.
2025-02-28 18:28:50 +00:00
0eaf2186b6 fix: zero-out D32 as well 2025-02-28 17:51:06 +00:00
5ae65d3190 fix: sanity check 2025-02-28 17:22:49 +00:00
68903f5b69 refactor: use explicit sizes in asmGenerator 2025-02-28 17:21:45 +00:00
e1d90eabf9 fix: use MemLocation.copy for zeroRest 2025-02-28 16:40:58 +00:00
1a39950a7b refactor: merge MemLocation with IndexedAddress 2025-02-28 16:26:40 +00:00
61643a49eb refactor: merge comments and extracting constants & renaming refactors
Merge request lab2425_spring/WACC_37!36

Co-authored-by: Jonny <j.sinteix@gmail.com>
Co-authored-by: Guy C <gc1523@ic.ac.uk>
2025-02-28 15:32:46 +00:00
82997a5a38 docs: clarify evalExprOntoStack sanity check, explanation comments for generateCall 2025-02-28 15:29:41 +00:00
720d9320e2 Merge branch 'master' into comments-and-refactors 2025-02-28 15:14:47 +00:00
c3f2ce8b19 refactor: single definition for common registers 2025-02-28 14:47:47 +00:00
Teixeira, Jonny
cf72c5250d refactor: merge labelGenerator refactors
Merge request lab2425_spring/WACC_37!35

Co-authored-by: Gleb Koval <gleb@koval.net>
2025-02-28 14:28:32 +00:00
7627ec14d2 refactor: return args and defs from labelGenerator, instead of strings 2025-02-28 14:23:09 +00:00
Jonny
d0a71c1888 docs: add doc for concatall chain extension 2025-02-28 14:07:50 +00:00
fb5799dbfd refactor: use LabelGenerator for RuntimeErrors 2025-02-28 14:07:00 +00:00
Jonny
621849dfa4 refactor: rename local builder chain to asm 2025-02-28 13:55:02 +00:00
967a6fe58b refactor: replace strings ListBuffer with labelGenerator 2025-02-28 13:14:29 +00:00
Guy C
302099ab76 refactor: removes magic numbers in asmgenerator 2025-02-28 12:23:09 +00:00
Guy C
30f4309fda feat: use errorcode constant in runtime errors 2025-02-28 12:14:55 +00:00
Guy C
b733d233b0 feat: implements outofmemoryerror handling 2025-02-28 12:01:31 +00:00
Guy C
f2a1eaf24c refactor: reorganize operation classes 2025-02-28 11:38:34 +00:00
8b3e9b8380 fix: ensure all advanced wacc-examples function correctly
Merge request lab2425_spring/WACC_37!33
2025-02-28 00:31:25 +00:00
41f76e50e0 fix: evaluate all function call arguments before setting registers 2025-02-28 00:27:26 +00:00
Teixeira, Jonny
88f89ce761 feat: implement all runtime errors
Merge request lab2425_spring/WACC_37!32

Co-authored-by: Guy C <gc1523@ic.ac.uk>
Co-authored-by: Jonny <j.sinteix@gmail.com>
Co-authored-by: Connolly, Guy <guy.connolly23@imperial.ac.uk>
Co-authored-by: Gleb Koval <gleb@koval.net>
2025-02-28 00:23:12 +00:00
Guy C
cdf32d93c3 feat: outofboundserror message includes given bad value 2025-02-28 00:17:30 +00:00
Jonny
edcac2782b fix: implements null pointer checks 2025-02-27 23:57:42 +00:00
Jonny
1b2df507ba fix: array bounds checks in place 2025-02-27 23:46:51 +00:00
Jonny
3a2af6f95d feat: implements outofbounds error. array negative bounds check added 2025-02-27 23:02:10 +00:00
Jonny
4727d6c399 fix: remove incorrect newline 2025-02-27 22:32:11 +00:00
Connolly, Guy
9639221a0a feat: merge labts fixes 2025-02-27 22:20:52 +00:00
Jonny
617f6759d3 fix: underflow detected 2025-02-27 21:53:48 +00:00
Jonny
6f5fcd4d85 refactor: redesigned runtime errors with added functionality 2025-02-27 21:48:28 +00:00
c31dd9de25 style: scala format 2025-02-27 20:00:07 +00:00
edce236158 fix: read space-delimited tokens (as per spec) 2025-02-27 19:58:57 +00:00
Guy C
9e6970de62 feat: implements overflow runtime error handling 2025-02-27 19:55:35 +00:00
cb4f899b8c test: provide stdin as space-delimited tokens 2025-02-27 19:50:46 +00:00
a20f28977b fix: handle runtime_error during testing 2025-02-27 19:47:33 +00:00
9a1728fb3f fix: exit from Main with exit code rather than voiding 2025-02-27 19:39:48 +00:00
Guy C
ea262e9a56 feat: implements divByZero and badChr runtime errors 2025-02-27 19:35:50 +00:00
Jonny
332c00b15b feat: added runtime errors class and object 2025-02-27 19:13:17 +00:00
9b9f0a80cb feat: x86 code generation implementation without runtime checking
Merge request lab2425_spring/WACC_37!29

Co-authored-by: Alex Ling <al4423@ic.ac.uk>
Co-authored-by: Jonny <j.sinteix@gmail.com>
Co-authored-by: Guy C <gc1523@ic.ac.uk>
Co-authored-by: Barf-Vader <47476490+Barf-Vader@users.noreply.github.com>
Co-authored-by: Ling, Alex <alex.ling23@imperial.ac.uk>
2025-02-27 18:54:54 +00:00
ada53e518b fix: variable-sized values, heap-allocated arrays (and printCharArray)
Merge request lab2425_spring/WACC_37!28

Co-authored-by: Alex Ling <al4423@ic.ac.uk>
2025-02-27 18:50:11 +00:00
c0f2473db1 test: fix input handling for IOLoop example 2025-02-27 18:25:12 +00:00
c472c7a62c fix: reserve return pointer and RBP on stack for user func bodies 2025-02-27 18:15:48 +00:00
507cb7dd9b fix: zero-out sub-32 bit expressions 2025-02-27 18:15:48 +00:00
887b982331 fix: variable-sized values, heap-allocated arrays (and printCharArray) 2025-02-27 18:15:48 +00:00
58df1d7bb9 refactor: extract Stack, proper register naming and sizes 2025-02-27 18:15:48 +00:00
Alex Ling
808a59f58a feat: almost implemented arrays 2025-02-27 18:15:48 +00:00
Jonny
bdee6ba756 feat: zero registers via xor 2025-02-27 15:48:48 +00:00
09df7af2ab fix: reset scope after all branching 2025-02-26 20:25:27 +00:00
2cf18a47a8 fix: only push one item to stack on comparisons 2025-02-26 20:00:50 +00:00
Jonny
631f9ddca5 feat: (maybe) tail call optimisation 2025-02-26 19:49:10 +00:00
4fb399a5e1 feat: generate microWacc for printing booleans 2025-02-26 19:14:12 +00:00
16de964f74 refactor: do not append epilogue to user functions since they all return anyway 2025-02-26 18:45:03 +00:00
c748a34e4c feat: user functions and calls 2025-02-26 18:31:15 +00:00
85190ce174 refactor: make microWacc.ArrayElem recursive rather than flat 2025-02-26 18:08:30 +00:00
Alex Ling
62df2c2244 fix: added dword in sizedir 2025-02-26 16:58:01 +00:00
Guy C
fb91dc89ee Merge branch 'branch-fix' into asm-gen 2025-02-26 07:21:26 +00:00
Guy C
07c67dbef6 feat: add zero division error handling in asmGenerator 2025-02-26 07:14:52 +00:00
39f88f6f8a test: disable parallel test execution to avoid race conditions 2025-02-26 03:31:11 +00:00
Guy C
fc2c58002e test: include variables tests in suite 2025-02-26 02:00:28 +00:00
Guy C
f15530149e fix: fix sub instruction code gen 2025-02-26 01:43:12 +00:00
Guy C
9ca50540e6 fix: fixed implementation of if statement code gen 2025-02-26 01:10:14 +00:00
Jonny
f76b7a9dc2 refactor: replace explicit loops and flatMap with foldMap 2025-02-25 22:46:48 +00:00
da0ef9ec24 ci: checkout commitlint back to current commit 2025-02-25 22:03:53 +00:00
64b015e494 ci: use JS commitlint configuration 2025-02-25 21:49:23 +00:00
Guy C
c9723f9359 feat: implements sign extension operation for division 2025-02-25 21:37:18 +00:00
70e023f27a Local Mutable Chains
Merge request lab2425_spring/WACC_37!27

Co-authored-by: Jonny <j.sinteix@gmail.com>
2025-02-25 21:13:46 +00:00
11c483439c fix: generate strDirs after prog, change += to + 2025-02-25 21:05:21 +00:00
Jonny
ebc65af981 feat: extension method concatAll defined on Chain implemented 2025-02-25 21:03:44 +00:00
Jonny
bd0eb76bec fix: alignment issue with stack in read 2025-02-25 21:02:59 +00:00
Jonny
edbc03ee25 feat: used local mutable Chains. Also implemented new LabelGenerator 2025-02-25 21:02:57 +00:00
Jonny
7953790f4d feat: used Chains instead of Lists 2025-02-25 21:01:02 +00:00
Alex Ling
7fd92b4212 refactor: passed exit and read tests 2025-02-25 18:25:34 +00:00
Alex Ling
87a239f37c fix: alignment issue with stack in read 2025-02-25 18:20:50 +00:00
4f3596b48a ci: fix check commits 2025-02-25 17:35:04 +00:00
efe9f91303 refactor: use non-singleton labelgenerator (instead use class) 2025-02-25 17:10:56 +00:00
5f8b87221c fix: escape characters within assembly 2025-02-25 17:07:21 +00:00
8558733719 style: scala format lexer 2025-02-25 16:33:17 +00:00
f628d16d3d fix: always push a value onto stack on expr evaluation 2025-02-25 16:27:47 +00:00
Alex Ling
3f76a2c5bf refactor: extract stack into seperate class 2025-02-25 04:44:08 +00:00
Alex Ling
8ed94e4df3 fix: initial exprs on stack 2025-02-25 03:17:05 +00:00
Guy C
58d280462e feat: enhance asmGenerator with additional registers and improve function call generation
Co-authored-by: Barf-Vader <Barf-Vader@users.noreply.github.com>
Co-authored-by: Gleb Koval
2025-02-25 02:02:57 +00:00
Guy C
f30cf42c4b style: improve code formatting and consistency in typeChecker and assemblyIR 2025-02-25 00:05:10 +00:00
Guy C
1488281223 feat: implements binary operators in asmGenerator
Co-authored-by: Gleb Koval <gleb@koval.net>
Co-authored-by: Barf-Vader <Barf-Vader@users.noreply.github.com>
2025-02-25 00:00:12 +00:00
Guy C
668d7338ae feat: move default return out of functionEpilogue into main def 2025-02-24 19:47:06 +00:00
Guy C
9d78caf6d9 feat: add support for return statements in asmGenerator 2025-02-24 18:57:13 +00:00
Guy C
909114bdce feat: implement basic while loop generation in asmGenerator 2025-02-24 04:47:21 +00:00
Guy C
2bed722a4f style: improve code formatting and readability in asmGenerator 2025-02-24 02:04:21 +00:00
Guy C
dc61b1e390 feat: implement label generation and basic conditional branching in asmGenerator 2025-02-24 02:00:35 +00:00
c59c28ecbd test: retrieve raw stdout in example tests, rather than lines 2025-02-22 23:38:19 +00:00
Guy C
82230a5f66 refactor: remove unused import of IntLiter in Main.scala 2025-02-22 22:53:42 +00:00
Guy C
1255a2e74c feat: add initialization of AX register in function prologue 2025-02-22 22:53:17 +00:00
Alex Ling
24dddcadab feat: almost complete clib calls 2025-02-22 21:38:12 +00:00
7f2870e340 feat: generate assembly from main 2025-02-21 23:35:54 +00:00
Barf-Vader
1ce36dd8da refactor: unit tests now work with asm ir refactor 2025-02-21 23:34:37 +00:00
Ling, Alex
de181d161d feat: merge master
Merge request lab2425_spring/WACC_37!25

Co-authored-by: Gleb Koval <gleb@koval.net>
2025-02-21 23:09:13 +00:00
Barf-Vader
ee4109e9cd style: fix style 2025-02-21 23:00:59 +00:00
Barf-Vader
02e741c52e feat: implemented println and exit 2025-02-21 22:53:20 +00:00
0391b9deba fix: remove logging from Main 2025-02-21 18:51:50 +00:00
ab28f0950a fix: main outputs to current dir 2025-02-21 18:46:10 +00:00
1c6ea16b6e feat: carrot mark + writer
Merge request lab2425_spring/WACC_37!24

Co-authored-by: Barf-Vader <47476490+Barf-Vader@users.noreply.github.com>
2025-02-21 18:35:06 +00:00
b2da8c2408 ci: use x86 image for tests 2025-02-21 18:30:33 +00:00
42ff9c9e79 test: backend tests 2025-02-21 18:19:23 +00:00
39c695b1bb feat: output backend files 2025-02-21 18:19:03 +00:00
87691902be feat: set wacc target to x86-64 2025-02-21 18:18:44 +00:00
eb7387b49c chore: update dependencies parsley-cats and os-lib 2025-02-21 18:18:35 +00:00
Barf-Vader
67f7e64b95 feat: add writer for assembly code 2025-02-20 19:47:24 +00:00
2a234f6db8 Assembly ir
Merge request lab2425_spring/WACC_37!23

Co-authored-by: Barf-Vader <47476490+Barf-Vader@users.noreply.github.com>
Co-authored-by: Guy C <gc1523@ic.ac.uk>
2025-02-20 19:02:47 +00:00
Barf-Vader
a8420ae45c Merge branch 'master' into assembly-ir 2025-02-20 19:00:11 +00:00
Barf-Vader
b692982d73 refactor: gitignore .idea/ 2025-02-20 18:57:41 +00:00
Guy C
006e85d0f8 style: improve formatting for Jump case class and condition toString method 2025-02-18 18:29:16 +00:00
Guy C
43682b902b test: add unit tests for register and instruction toString methods 2025-02-18 18:27:42 +00:00
Guy C
d49c267d50 feat: extends IR for load/move instructions and generalised conditional jumps 2025-02-18 18:26:50 +00:00
bb090ad431 feat: microwacc type checker implementation
Merge request lab2425_spring/WACC_37!22
2025-02-18 17:29:10 +00:00
8991024d5d feat: initial microWacc definition
Merge request lab2425_spring/WACC_37!21
2025-02-18 17:27:12 +00:00
Barf-Vader
2c281066a8 refactor: used varargs instead of noOperand 2025-02-17 18:07:36 +00:00
27cc25cc0d feat: type-checker returns micro wacc 2025-02-17 15:26:32 +00:00
Barf-Vader
7525e523bb style: reformatted to pass stylecheck 2025-02-16 18:03:39 +00:00
Barf-Vader
4e58e41a2a refactor: abstracted operations, used trait Operand 2025-02-16 17:58:35 +00:00
b7e442b269 refactor: introduce exit-code guard against InternalError 2025-02-14 00:35:57 +00:00
756b42dd72 refactor: remove implicit ast from type checker 2025-02-14 00:35:57 +00:00
bc25f914ad fix: add uid to microWacc Ident 2025-02-14 00:35:48 +00:00
6a6aadbbeb fix: add nulliter to micro wacc 2025-02-14 00:21:10 +00:00
03999e00ef fix: add support for println in micro wacc 2025-02-14 00:07:43 +00:00
d6aa83a2ea fix: add support for return types in micro wacc calls 2025-02-13 23:59:44 +00:00
e23ef8da48 feat: initial microWacc definition 2025-02-13 23:07:38 +00:00
Barf-Vader
32622cdd7e feat: incomplete initial implementation of assemblyIR 2025-02-13 17:42:50 +00:00
Guy C
41ed06f91c refactor: moved frontend files into dedicated folder 2025-02-11 21:36:06 +00:00
25 changed files with 1822 additions and 417 deletions

4
.commitlintrc.js Normal file
View File

@@ -0,0 +1,4 @@
export default {
extends: ['@commitlint/config-conventional'],
ignores: [commit => commit.startsWith("Local Mutable Chains\n")]
}

View File

@@ -1 +0,0 @@
extends: "@commitlint/config-conventional"

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@
.scala-build/
.vscode/
wacc-examples/
.idea/

View File

@@ -30,9 +30,10 @@ check_commits:
before_script:
- apk add git
- npm install -g @commitlint/cli @commitlint/config-conventional
- git pull origin master
- git checkout origin/master
script:
- npx commitlint --from origin/master --to HEAD --verbose
- git checkout ${CI_COMMIT_SHA}
- npx commitlint --from origin/master --to ${CI_COMMIT_SHA} --verbose
compile_jvm:
stage: compile
@@ -45,6 +46,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]

View File

@@ -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

View File

@@ -1,9 +1,13 @@
package wacc
import scala.collection.mutable
import cats.data.Chain
import parsley.{Failure, Success}
import scopt.OParser
import java.io.File
import java.io.PrintStream
import assemblyIR as asm
case class CliConfig(
file: File = new File(".")
@@ -30,27 +34,59 @@ 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
val (names, funcs) = renamer.rename(prog)
given ctx: typeChecker.TypeCheckerCtx = typeChecker.TypeCheckerCtx(names, funcs, errors)
typeChecker.check(prog)
val typedProg = typeChecker.check(prog)
if (errors.result.nonEmpty) {
given errorContent: String = contents
errors.result.foreach(printError)
200
} else 0
case Failure(msg) =>
println(msg)
100
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"
def backend(typedProg: microWacc.Program): Chain[asm.AsmLine] =
asmGenerator.generateAsm(typedProg)
def compile(filename: String, outFile: Option[File] = None)(using
stdout: PrintStream = Console.out
): Int =
frontend(os.read(os.Path(filename))) match {
case Left(typedProg) =>
val asmFile = outFile.getOrElse(File(filename.stripSuffix(".wacc") + ".s"))
val asm = backend(typedProg)
writer.writeTo(asm, PrintStream(asmFile))
0
case Right(exitCode) => exitCode
}
def main(args: Array[String]): Unit =
OParser.parse(cliParser, args, CliConfig()) match {
case Some(config) =>
System.exit(compile(os.read(os.Path(config.file.getAbsolutePath))))
System.exit(
compile(
config.file.getAbsolutePath,
outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s"))
)
)
case None =>
}

View File

@@ -0,0 +1,50 @@
package wacc
import scala.collection.mutable
import cats.data.Chain
private class LabelGenerator {
import assemblyIR._
import microWacc.{CallTarget, Ident, Builtin}
import asmGenerator.escaped
private val strings = mutable.HashMap[String, String]()
private var labelVal = -1
/** Get an arbitrary label. */
def getLabel(): String = {
labelVal += 1
s".L$labelVal"
}
private def getLabel(target: CallTarget | RuntimeError): String = target match {
case Ident(v, _) => s"wacc_$v"
case Builtin(name) => s"_$name"
case err: RuntimeError => s".L.${err.name}"
}
/** Get a named label def for a function or error. */
def getLabelDef(target: CallTarget | RuntimeError): LabelDef =
LabelDef(getLabel(target))
/** Get a named label for a function or error. */
def getLabelArg(target: CallTarget | RuntimeError): LabelArg =
LabelArg(getLabel(target))
/** Get an arbitrary label for a string. */
def getLabelArg(str: String): LabelArg =
LabelArg(strings.getOrElseUpdate(str, s".L.str${strings.size}"))
/** Get a named label for a string. */
def getLabelArg(src: String, name: String): LabelArg =
LabelArg(strings.getOrElseUpdate(src, s".L.$name.str${strings.size}"))
/** Generate the assembly labels for constants that were labelled using the LabelGenerator. */
def generateConstants: Chain[AsmLine] =
strings.foldLeft(Chain.empty) { case (acc, (str, label)) =>
acc ++ Chain(
LabelDef(label),
Directive.Asciz(str.escaped)
)
}
}

View File

@@ -0,0 +1,112 @@
package wacc
import cats.data.Chain
import wacc.assemblyIR._
sealed trait RuntimeError {
val name: String
protected val errStr: String
protected def getErrLabel(using labelGenerator: LabelGenerator): LabelArg =
labelGenerator.getLabelArg(errStr, name = name)
protected def generateHandler(using labelGenerator: LabelGenerator): Chain[AsmLine]
def generate(using labelGenerator: LabelGenerator): Chain[AsmLine] =
labelGenerator.getLabelDef(this) +: generateHandler
}
object RuntimeError {
import wacc.asmGenerator.stackAlign
import assemblyIR.commonRegisters._
private val ERROR_CODE = 255
case object ZeroDivError extends RuntimeError {
val name = "errDivZero"
protected val errStr = "fatal error: division or modulo by zero"
protected def generateHandler(using labelGenerator: LabelGenerator): Chain[AsmLine] = Chain(
stackAlign,
Load(RDI, MemLocation(RIP, getErrLabel)),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(-1)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object BadChrError extends RuntimeError {
val name = "errBadChr"
protected val errStr = "fatal error: int %d is not an ASCII character 0-127"
protected def generateHandler(using labelGenerator: LabelGenerator): Chain[AsmLine] = Chain(
Pop(RSI),
stackAlign,
Load(RDI, MemLocation(RIP, getErrLabel)),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(ERROR_CODE)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object NullPtrError extends RuntimeError {
val name = "errNullPtr"
protected val errStr = "fatal error: null pair dereferenced or freed"
protected def generateHandler(using labelGenerator: LabelGenerator): Chain[AsmLine] = Chain(
stackAlign,
Load(RDI, MemLocation(RIP, getErrLabel)),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(ERROR_CODE)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object OverflowError extends RuntimeError {
val name = "errOverflow"
protected val errStr = "fatal error: integer overflow or underflow occurred"
protected def generateHandler(using labelGenerator: LabelGenerator): Chain[AsmLine] = Chain(
stackAlign,
Load(RDI, MemLocation(RIP, getErrLabel)),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(ERROR_CODE)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object OutOfBoundsError extends RuntimeError {
val name = "errOutOfBounds"
protected val errStr = "fatal error: array index %d out of bounds"
protected def generateHandler(using labelGenerator: LabelGenerator): Chain[AsmLine] = Chain(
Move(RSI, RCX),
stackAlign,
Load(RDI, MemLocation(RIP, getErrLabel)),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(ERROR_CODE)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object OutOfMemoryError extends RuntimeError {
val name = "errOutOfMemory"
protected val errStr = "fatal error: out of memory"
def generateHandler(using labelGenerator: LabelGenerator): Chain[AsmLine] = Chain(
stackAlign,
Load(RDI, MemLocation(RIP, getErrLabel)),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(ERROR_CODE)),
assemblyIR.Call(CLibFunc.Exit)
)
}
val all: Chain[RuntimeError] =
Chain(ZeroDivError, BadChrError, NullPtrError, OverflowError, OutOfBoundsError,
OutOfMemoryError)
}

View File

@@ -0,0 +1,90 @@
package wacc
import scala.collection.mutable.LinkedHashMap
import cats.data.Chain
class Stack {
import assemblyIR._
import assemblyIR.Size._
import sizeExtensions.size
import microWacc as mw
private val RSP = Register(Q64, RegName.SP)
private class StackValue(val size: Size, val offset: Int) {
def bottom: Int = offset + elemBytes
}
private val stack = LinkedHashMap[mw.Expr | Int, StackValue]()
private val elemBytes: Int = Q64.toInt
private def sizeBytes: Int = stack.size * elemBytes
/** The stack's size in bytes. */
def size: Int = stack.size
/** Push an expression onto the stack. */
def push(expr: mw.Expr, src: Register): AsmLine = {
stack += expr -> StackValue(src.size, sizeBytes)
Push(src)
}
/** Push a value onto the stack. */
def push(itemSize: Size, addr: Src): AsmLine = {
stack += stack.size -> StackValue(itemSize, sizeBytes)
Push(addr)
}
/** Reserve space for a variable on the stack. */
def reserve(ident: mw.Ident): AsmLine = {
stack += ident -> StackValue(ident.ty.size, sizeBytes)
Subtract(RSP, ImmediateVal(elemBytes))
}
/** Reserve space for a register on the stack. */
def reserve(src: Register): AsmLine = {
stack += stack.size -> StackValue(src.size, sizeBytes)
Subtract(RSP, ImmediateVal(src.size.toInt))
}
/** Reserve space for values on the stack.
*
* @param sizes
* The sizes of the values to reserve space for.
*/
def reserve(sizes: Size*): AsmLine = {
sizes.foreach { itemSize =>
stack += stack.size -> StackValue(itemSize, sizeBytes)
}
Subtract(RSP, ImmediateVal(elemBytes * sizes.size))
}
/** Pop a value from the stack into a register. Sizes MUST match. */
def pop(dest: Register): AsmLine = {
stack.remove(stack.last._1)
Pop(dest)
}
/** Drop the top n values from the stack. */
def drop(n: Int = 1): AsmLine = {
(1 to n).foreach { _ =>
stack.remove(stack.last._1)
}
Add(RSP, ImmediateVal(n * elemBytes))
}
/** Generate AsmLines within a scope, which is reset after the block. */
def withScope(block: () => Chain[AsmLine]): Chain[AsmLine] = {
val resetToSize = stack.size
var lines = block()
lines :+= drop(stack.size - resetToSize)
lines
}
/** Get an MemLocation for a variable in the stack. */
def accessVar(ident: mw.Ident): MemLocation =
MemLocation(RSP, sizeBytes - stack(ident).bottom)
def contains(ident: mw.Ident): Boolean = stack.contains(ident)
def head: MemLocation = MemLocation(RSP, opSize = Some(stack.last._2.size))
override def toString(): String = stack.toString
}

View File

@@ -0,0 +1,472 @@
package wacc
import cats.data.Chain
import cats.syntax.foldable._
import wacc.RuntimeError._
object asmGenerator {
import microWacc._
import assemblyIR._
import assemblyIR.commonRegisters._
import assemblyIR.RegName._
import types._
import sizeExtensions._
import lexer.escapedChars
private val argRegs = List(DI, SI, DX, CX, R8, R9)
private val _7_BIT_MASK = 0x7f
extension [T](chain: Chain[T])
def +(item: T): Chain[T] = chain.append(item)
/** Concatenates multiple `Chain[T]` instances into a single `Chain[T]`, appending them to the
* current `Chain`.
*
* @param chains
* A variable number of `Chain[T]` instances to concatenate.
* @return
* A new `Chain[T]` containing all elements from `chain` concatenated with `chains`.
*/
def concatAll(chains: Chain[T]*): Chain[T] =
chains.foldLeft(chain)(_ ++ _)
def generateAsm(microProg: Program): Chain[AsmLine] = {
given stack: Stack = Stack()
given labelGenerator: LabelGenerator = LabelGenerator()
val Program(funcs, main) = microProg
val progAsm = Chain(LabelDef("main")).concatAll(
funcPrologue(),
main.foldMap(generateStmt(_)),
Chain.one(Xor(RAX, RAX)),
funcEpilogue(),
generateBuiltInFuncs(),
RuntimeError.all.foldMap(_.generate),
funcs.foldMap(generateUserFunc(_))
)
Chain(
Directive.IntelSyntax,
Directive.Global("main"),
Directive.RoData
).concatAll(
labelGenerator.generateConstants,
Chain.one(Directive.Text),
progAsm
)
}
private def wrapBuiltinFunc(builtin: Builtin, funcBody: Chain[AsmLine])(using
stack: Stack,
labelGenerator: LabelGenerator
): Chain[AsmLine] = {
var asm = Chain.one[AsmLine](labelGenerator.getLabelDef(builtin))
asm ++= funcPrologue()
asm ++= funcBody
asm ++= funcEpilogue()
asm
}
private def generateUserFunc(func: FuncDecl)(using
labelGenerator: LabelGenerator
): Chain[AsmLine] = {
given stack: Stack = Stack()
// Setup the stack with param 7 and up
func.params.drop(argRegs.size).foreach(stack.reserve(_))
stack.reserve(Size.Q64) // Reserve return pointer slot
var asm = Chain.one[AsmLine](labelGenerator.getLabelDef(func.name))
asm ++= funcPrologue()
// Push the rest of params onto the stack for simplicity
argRegs.zip(func.params).foreach { (reg, param) =>
asm += stack.push(param, Register(Size.Q64, reg))
}
asm ++= func.body.foldMap(generateStmt(_))
// No need for epilogue here since all user functions must return explicitly
asm
}
private def generateBuiltInFuncs()(using
stack: Stack,
labelGenerator: LabelGenerator
): Chain[AsmLine] = {
var asm = Chain.empty[AsmLine]
asm ++= wrapBuiltinFunc(
Builtin.Exit,
Chain(stackAlign, assemblyIR.Call(CLibFunc.Exit))
)
asm ++= wrapBuiltinFunc(
Builtin.Printf,
Chain(
stackAlign,
assemblyIR.Call(CLibFunc.PrintF),
Xor(RDI, RDI),
assemblyIR.Call(CLibFunc.Fflush)
)
)
asm ++= wrapBuiltinFunc(
Builtin.PrintCharArray,
Chain(
stackAlign,
Load(RDX, MemLocation(RSI, KnownType.Int.size.toInt)),
Move(Register(KnownType.Int.size, SI), MemLocation(RSI, opSize = Some(KnownType.Int.size))),
assemblyIR.Call(CLibFunc.PrintF),
Xor(RDI, RDI),
assemblyIR.Call(CLibFunc.Fflush)
)
)
asm ++= wrapBuiltinFunc(
Builtin.Malloc,
Chain(
stackAlign,
assemblyIR.Call(CLibFunc.Malloc),
// Out of memory check
Compare(RAX, ImmediateVal(0)),
Jump(labelGenerator.getLabelArg(OutOfMemoryError), Cond.Equal)
)
)
asm ++= wrapBuiltinFunc(
Builtin.Free,
Chain(
stackAlign,
Compare(RDI, ImmediateVal(0)),
Jump(labelGenerator.getLabelArg(NullPtrError), Cond.Equal),
assemblyIR.Call(CLibFunc.Free)
)
)
asm ++= wrapBuiltinFunc(
Builtin.Read,
Chain(
stackAlign,
Subtract(Register(Size.Q64, SP), ImmediateVal(8)),
Push(RSI),
Load(RSI, MemLocation(Register(Size.Q64, SP), opSize = Some(Size.Q64))),
assemblyIR.Call(CLibFunc.Scanf),
Pop(RAX)
)
)
asm
}
private def generateStmt(stmt: Stmt)(using
stack: Stack,
labelGenerator: LabelGenerator
): Chain[AsmLine] = {
var asm = Chain.empty[AsmLine]
asm += Comment(stmt.toString)
stmt match {
case Assign(lhs, rhs) =>
lhs match {
case ident: Ident =>
if (!stack.contains(ident)) asm += stack.reserve(ident)
asm ++= evalExprOntoStack(rhs)
asm += stack.pop(RAX)
asm += Move(stack.accessVar(ident).copy(opSize = Some(Size.Q64)), RAX)
case ArrayElem(x, i) =>
asm ++= evalExprOntoStack(rhs)
asm ++= evalExprOntoStack(i)
asm += stack.pop(RCX)
asm += Compare(ECX, ImmediateVal(0))
asm += Jump(labelGenerator.getLabelArg(OutOfBoundsError), Cond.Less)
asm += stack.push(KnownType.Int.size, RCX)
asm ++= evalExprOntoStack(x)
asm += stack.pop(RAX)
asm += stack.pop(RCX)
asm += Compare(RAX, ImmediateVal(0))
asm += Jump(labelGenerator.getLabelArg(NullPtrError), Cond.Equal)
asm += Compare(MemLocation(RAX, opSize = Some(KnownType.Int.size)), ECX)
asm += Jump(labelGenerator.getLabelArg(OutOfBoundsError), Cond.LessEqual)
asm += stack.pop(RDX)
asm += Move(
MemLocation(RAX, KnownType.Int.size.toInt, (RCX, x.ty.elemSize.toInt)),
Register(x.ty.elemSize, DX)
)
}
case If(cond, thenBranch, elseBranch) =>
val elseLabel = labelGenerator.getLabel()
val endLabel = labelGenerator.getLabel()
asm ++= evalExprOntoStack(cond)
asm += stack.pop(RAX)
asm += Compare(RAX, ImmediateVal(0))
asm += Jump(LabelArg(elseLabel), Cond.Equal)
asm ++= stack.withScope(() => thenBranch.foldMap(generateStmt))
asm += Jump(LabelArg(endLabel))
asm += LabelDef(elseLabel)
asm ++= stack.withScope(() => elseBranch.foldMap(generateStmt))
asm += LabelDef(endLabel)
case While(cond, body) =>
val startLabel = labelGenerator.getLabel()
val endLabel = labelGenerator.getLabel()
asm += LabelDef(startLabel)
asm ++= evalExprOntoStack(cond)
asm += stack.pop(RAX)
asm += Compare(RAX, ImmediateVal(0))
asm += Jump(LabelArg(endLabel), Cond.Equal)
asm ++= stack.withScope(() => body.foldMap(generateStmt))
asm += Jump(LabelArg(startLabel))
asm += LabelDef(endLabel)
case call: microWacc.Call =>
asm ++= generateCall(call, isTail = false)
case microWacc.Return(expr) =>
expr match {
case call: microWacc.Call =>
asm ++= generateCall(call, isTail = true) // tco
case _ =>
asm ++= evalExprOntoStack(expr)
asm += stack.pop(RAX)
asm ++= funcEpilogue()
}
}
asm
}
private def evalExprOntoStack(expr: Expr)(using
stack: Stack,
labelGenerator: LabelGenerator
): Chain[AsmLine] = {
var asm = Chain.empty[AsmLine]
val stackSizeStart = stack.size
expr match {
case IntLiter(v) => asm += stack.push(KnownType.Int.size, ImmediateVal(v))
case CharLiter(v) => asm += stack.push(KnownType.Char.size, ImmediateVal(v.toInt))
case ident: Ident =>
val location = stack.accessVar(ident)
// items in stack are guaranteed to be in Q64 slots,
// so we are safe to wipe the opSize from the memory location
asm += stack.push(ident.ty.size, location.copy(opSize = None))
case array @ ArrayLiter(elems) =>
expr.ty match {
case KnownType.String =>
val str = elems.collect { case CharLiter(v) => v }.mkString
asm += Load(RAX, MemLocation(RIP, labelGenerator.getLabelArg(str)))
asm += stack.push(KnownType.String.size, RAX)
case ty =>
asm ++= generateCall(
microWacc.Call(Builtin.Malloc, List(IntLiter(array.heapSize))),
isTail = false
)
asm += stack.push(KnownType.Array(?).size, RAX)
// Store the length of the array at the start
asm += Move(
MemLocation(RAX, opSize = Some(KnownType.Int.size)),
ImmediateVal(elems.size)
)
elems.zipWithIndex.foldMap { (elem, i) =>
asm ++= evalExprOntoStack(elem)
asm += stack.pop(RCX)
asm += stack.pop(RAX)
asm += Move(
MemLocation(RAX, KnownType.Int.size.toInt + i * ty.elemSize.toInt),
Register(ty.elemSize, CX)
)
asm += stack.push(KnownType.Array(?).size, RAX)
}
}
case BoolLiter(true) =>
asm += stack.push(KnownType.Bool.size, ImmediateVal(1))
case BoolLiter(false) =>
asm += Xor(RAX, RAX)
asm += stack.push(KnownType.Bool.size, RAX)
case NullLiter() =>
asm += stack.push(KnownType.Pair(?, ?).size, ImmediateVal(0))
case ArrayElem(x, i) =>
asm ++= evalExprOntoStack(x)
asm ++= evalExprOntoStack(i)
asm += stack.pop(RCX)
asm += Compare(RCX, ImmediateVal(0))
asm += Jump(labelGenerator.getLabelArg(OutOfBoundsError), Cond.Less)
asm += stack.pop(RAX)
asm += Compare(RAX, ImmediateVal(0))
asm += Jump(labelGenerator.getLabelArg(NullPtrError), Cond.Equal)
asm += Compare(MemLocation(RAX, opSize = Some(KnownType.Int.size)), ECX)
asm += Jump(labelGenerator.getLabelArg(OutOfBoundsError), Cond.LessEqual)
// + Int because we store the length of the array at the start
asm += Move(
Register(x.ty.elemSize, AX),
MemLocation(RAX, KnownType.Int.size.toInt, (RCX, x.ty.elemSize.toInt))
)
asm += stack.push(x.ty.elemSize, RAX)
case UnaryOp(x, op) =>
asm ++= evalExprOntoStack(x)
op match {
case UnaryOperator.Chr =>
asm += Move(EAX, stack.head)
asm += And(EAX, ImmediateVal(~_7_BIT_MASK))
asm += Compare(EAX, ImmediateVal(0))
asm += Jump(labelGenerator.getLabelArg(BadChrError), Cond.NotEqual)
case UnaryOperator.Ord => // No op needed
case UnaryOperator.Len =>
asm += stack.pop(RAX)
asm += Move(EAX, MemLocation(RAX, opSize = Some(KnownType.Int.size)))
asm += stack.push(KnownType.Int.size, RAX)
case UnaryOperator.Negate =>
asm += Xor(EAX, EAX)
asm += Subtract(EAX, stack.head)
asm += Jump(labelGenerator.getLabelArg(OverflowError), Cond.Overflow)
asm += stack.drop()
asm += stack.push(KnownType.Int.size, RAX)
case UnaryOperator.Not =>
asm += Xor(stack.head, ImmediateVal(1))
}
case BinaryOp(x, y, op) =>
val destX = Register(x.ty.size, AX)
asm ++= evalExprOntoStack(y)
asm ++= evalExprOntoStack(x)
asm += stack.pop(RAX)
op match {
case BinaryOperator.Add =>
asm += Add(stack.head, destX)
asm += Jump(labelGenerator.getLabelArg(OverflowError), Cond.Overflow)
case BinaryOperator.Sub =>
asm += Subtract(destX, stack.head)
asm += Jump(labelGenerator.getLabelArg(OverflowError), Cond.Overflow)
asm += stack.drop()
asm += stack.push(destX.size, RAX)
case BinaryOperator.Mul =>
asm += Multiply(destX, stack.head)
asm += Jump(labelGenerator.getLabelArg(OverflowError), Cond.Overflow)
asm += stack.drop()
asm += stack.push(destX.size, RAX)
case BinaryOperator.Div =>
asm += Compare(stack.head, ImmediateVal(0))
asm += Jump(labelGenerator.getLabelArg(ZeroDivError), Cond.Equal)
asm += CDQ()
asm += Divide(stack.head)
asm += stack.drop()
asm += stack.push(destX.size, RAX)
case BinaryOperator.Mod =>
asm += Compare(stack.head, ImmediateVal(0))
asm += Jump(labelGenerator.getLabelArg(ZeroDivError), Cond.Equal)
asm += CDQ()
asm += Divide(stack.head)
asm += stack.drop()
asm += stack.push(destX.size, RDX)
case BinaryOperator.Eq => asm ++= generateComparison(destX, Cond.Equal)
case BinaryOperator.Neq => asm ++= generateComparison(destX, Cond.NotEqual)
case BinaryOperator.Greater => asm ++= generateComparison(destX, Cond.Greater)
case BinaryOperator.GreaterEq => asm ++= generateComparison(destX, Cond.GreaterEqual)
case BinaryOperator.Less => asm ++= generateComparison(destX, Cond.Less)
case BinaryOperator.LessEq => asm ++= generateComparison(destX, Cond.LessEqual)
case BinaryOperator.And => asm += And(stack.head, destX)
case BinaryOperator.Or => asm += Or(stack.head, destX)
}
case call: microWacc.Call =>
asm ++= generateCall(call, isTail = false)
asm += stack.push(call.ty.size, RAX)
}
assert(
stack.size == stackSizeStart + 1,
"Sanity check: ONLY the evaluated expression should have been pushed onto the stack"
)
asm ++= zeroRest(stack.head.copy(opSize = Some(Size.Q64)), expr.ty.size)
asm
}
private def generateCall(call: microWacc.Call, isTail: Boolean)(using
stack: Stack,
labelGenerator: LabelGenerator
): Chain[AsmLine] = {
var asm = Chain.empty[AsmLine]
val microWacc.Call(target, args) = call
// Evaluate arguments 0-6
argRegs
.zip(args)
.map { (reg, expr) =>
asm ++= evalExprOntoStack(expr)
reg
}
// And set the appropriate registers
.reverse
.foreach { reg =>
asm += stack.pop(Register(Size.Q64, reg))
}
// Evaluate arguments 7 and up and push them onto the stack
args.drop(argRegs.size).foldMap {
asm ++= evalExprOntoStack(_)
}
// Tail Call Optimisation (TCO)
if (isTail) {
asm += Jump(labelGenerator.getLabelArg(target)) // tail call
} else {
asm += assemblyIR.Call(labelGenerator.getLabelArg(target)) // regular call
}
// Remove arguments 7 and up from the stack
if (args.size > argRegs.size) {
asm += stack.drop(args.size - argRegs.size)
}
asm
}
private def generateComparison(destX: Register, cond: Cond)(using
stack: Stack
): Chain[AsmLine] = {
var asm = Chain.empty[AsmLine]
asm += Compare(destX, stack.head)
asm += Set(Register(Size.B8, AX), cond)
asm ++= zeroRest(RAX, Size.B8)
asm += stack.drop()
asm += stack.push(Size.B8, RAX)
asm
}
private def funcPrologue()(using stack: Stack): Chain[AsmLine] = {
var asm = Chain.empty[AsmLine]
asm += stack.push(Size.Q64, RBP)
asm += Move(RBP, Register(Size.Q64, SP))
asm
}
private def funcEpilogue(): Chain[AsmLine] = {
var asm = Chain.empty[AsmLine]
asm += Move(Register(Size.Q64, SP), RBP)
asm += Pop(RBP)
asm += assemblyIR.Return()
asm
}
def stackAlign: AsmLine = And(Register(Size.Q64, SP), ImmediateVal(-16))
private def zeroRest(dest: Dest, size: Size): Chain[AsmLine] = size match {
case Size.Q64 | Size.D32 => Chain.empty
case _ => Chain.one(And(dest, ImmediateVal((1 << (size.toInt * 8)) - 1)))
}
private val escapedCharsMapping = escapedChars.map { case (k, v) => v -> s"\\$k" }
extension (s: String) {
def escaped: String =
s.flatMap(c => escapedCharsMapping.getOrElse(c, c.toString))
}
}

View File

@@ -0,0 +1,241 @@
package wacc
object assemblyIR {
sealed trait AsmLine
sealed trait Operand
sealed trait Src extends Operand // mem location, register and imm value
sealed trait Dest extends Operand // mem location and register
enum Size {
case Q64, D32, W16, B8
def toInt: Int = this match {
case Q64 => 8
case D32 => 4
case W16 => 2
case B8 => 1
}
private val ptr = "ptr "
override def toString(): String = this match {
case Q64 => "qword " + ptr
case D32 => "dword " + ptr
case W16 => "word " + ptr
case B8 => "byte " + ptr
}
}
enum RegName {
case AX, BX, CX, DX, SI, DI, SP, BP, IP, R8, R9, R10, R11, R12, R13, R14, R15
}
case class Register(size: Size, name: RegName) extends Dest with Src {
import RegName._
if (size == Size.B8 && name == RegName.IP) {
throw new IllegalArgumentException("Cannot have 8 bit register for IP")
}
override def toString = name match {
case AX => tradToString("ax", "al")
case BX => tradToString("bx", "bl")
case CX => tradToString("cx", "cl")
case DX => tradToString("dx", "dl")
case SI => tradToString("si", "sil")
case DI => tradToString("di", "dil")
case SP => tradToString("sp", "spl")
case BP => tradToString("bp", "bpl")
case IP => tradToString("ip", "#INVALID")
case R8 => newToString(8)
case R9 => newToString(9)
case R10 => newToString(10)
case R11 => newToString(11)
case R12 => newToString(12)
case R13 => newToString(13)
case R14 => newToString(14)
case R15 => newToString(15)
}
private def tradToString(base: String, byteName: String): String =
size match {
case Size.Q64 => "r" + base
case Size.D32 => "e" + base
case Size.W16 => base
case Size.B8 => byteName
}
private def newToString(base: Int): String = {
val b = base.toString
"r" + (size match {
case Size.Q64 => b
case Size.D32 => b + "d"
case Size.W16 => b + "w"
case Size.B8 => b + "b"
})
}
}
// arguments
enum CLibFunc extends Operand {
case Scanf,
Fflush,
Exit,
PrintF,
Malloc,
Free
private val plt = "@plt"
override def toString = this match {
case Scanf => "scanf" + plt
case Fflush => "fflush" + plt
case Exit => "exit" + plt
case PrintF => "printf" + plt
case Malloc => "malloc" + plt
case Free => "free" + plt
}
}
case class MemLocation(
base: Register,
offset: Int | LabelArg = 0,
// scale 0 will make register irrelevant, no other reason as to why it's RAX
scaledIndex: (Register, Int) = (Register(Size.Q64, RegName.AX), 0),
opSize: Option[Size] = None
) extends Dest
with Src {
def copy(
base: Register = this.base,
offset: Int | LabelArg = this.offset,
scaledIndex: (Register, Int) = this.scaledIndex,
opSize: Option[Size] = this.opSize
): MemLocation = MemLocation(base, offset, scaledIndex, opSize)
override def toString(): String = {
val opSizeStr = opSize.map(_.toString).getOrElse("")
val baseStr = base.toString
val offsetStr = offset match {
case 0 => ""
case off => s" + $off"
}
val scaledIndexStr = scaledIndex match {
case (reg, scale) if scale != 0 => s" + $reg * $scale"
case _ => ""
}
s"$opSizeStr[$baseStr$scaledIndexStr$offsetStr]"
}
}
case class ImmediateVal(value: Int) extends Src {
override def toString = value.toString
}
case class LabelArg(name: String) extends Operand {
override def toString = name
}
abstract class Operation(ins: String, ops: Operand*) extends AsmLine {
override def toString: String = s"\t$ins ${ops.mkString(", ")}"
}
// arithmetic operations
case class Add(op1: Dest, op2: Src) extends Operation("add", op1, op2)
case class Subtract(op1: Dest, op2: Src) extends Operation("sub", op1, op2)
case class Multiply(ops: Operand*) extends Operation("imul", ops*)
case class Divide(op1: Src) extends Operation("idiv", op1)
case class Negate(op: Dest) extends Operation("neg", op)
// bitwise operations
case class And(op1: Dest, op2: Src) extends Operation("and", op1, op2)
case class Or(op1: Dest, op2: Src) extends Operation("or", op1, op2)
case class Xor(op1: Dest, op2: Src) extends Operation("xor", op1, op2)
case class Compare(op1: Dest, op2: Src) extends Operation("cmp", op1, op2)
case class CDQ() extends Operation("cdq")
// stack operations
case class Push(op1: Src) extends Operation("push", op1)
case class Pop(op1: Src) extends Operation("pop", op1)
// move operations
case class Move(op1: Dest, op2: Src) extends Operation("mov", op1, op2)
case class Load(op1: Register, op2: MemLocation) extends Operation("lea ", op1, op2)
// function call operations
case class Call(op1: CLibFunc | LabelArg) extends Operation("call", op1)
case class Return() extends Operation("ret")
// conditional operations
case class Jump(op1: LabelArg, condition: Cond = Cond.Always)
extends Operation(s"j${condition.toString}", op1)
case class Set(op1: Dest, condition: Cond = Cond.Always)
extends Operation(s"set${condition.toString}", op1)
case class LabelDef(name: String) extends AsmLine {
override def toString = s"$name:"
}
case class Comment(comment: String) extends AsmLine {
override def toString =
comment.split("\n").map(line => s"# ${line}").mkString("\n")
}
enum Cond {
case Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
Overflow,
Always
override def toString(): String = this match {
case Equal => "e"
case NotEqual => "ne"
case Greater => "g"
case GreaterEqual => "ge"
case Less => "l"
case LessEqual => "le"
case Overflow => "o"
case Always => "mp"
}
}
enum Directive extends AsmLine {
case IntelSyntax, RoData, Text
case Global(name: String)
case Int(value: scala.Int)
case Asciz(string: String)
override def toString(): String = this match {
case IntelSyntax => ".intel_syntax noprefix"
case Global(name) => s".globl $name"
case Text => ".text"
case RoData => ".section .rodata"
case Int(value) => s"\t.int $value"
case Asciz(string) => s"\t.asciz \"$string\""
}
}
enum PrintFormat {
case Int, Char, String
override def toString(): String = this match {
case Int => "%d"
case Char => "%c"
case String => "%s"
}
}
object commonRegisters {
import Size._
import RegName._
val RAX = Register(Q64, AX)
val EAX = Register(D32, AX)
val RDI = Register(Q64, DI)
val RIP = Register(Q64, IP)
val RBP = Register(Q64, BP)
val RSI = Register(Q64, SI)
val RDX = Register(Q64, DX)
val RCX = Register(Q64, CX)
val ECX = Register(D32, CX)
}
}

View File

@@ -0,0 +1,33 @@
package wacc
object sizeExtensions {
import microWacc._
import types._
import assemblyIR.Size
extension (expr: Expr) {
/** Calculate the size (bytes) of the heap required for the expression. */
def heapSize: Int = (expr, expr.ty) match {
case (ArrayLiter(elems), ty) =>
KnownType.Int.size.toInt + elems.size * ty.elemSize.toInt
case _ => expr.ty.size.toInt
}
}
extension (ty: SemType) {
/** Calculate the size (bytes) of a type in a register. */
def size: Size = ty match {
case KnownType.Int => Size.D32
case KnownType.Bool | KnownType.Char => Size.B8
case KnownType.String | KnownType.Array(_) | KnownType.Pair(_, _) | ? => Size.Q64
}
def elemSize: Size = ty match {
case KnownType.Array(elem) => elem.size
case KnownType.Pair(_, _) => Size.Q64
case _ => ty.size
}
}
}

View File

@@ -0,0 +1,12 @@
package wacc
import java.io.PrintStream
import cats.data.Chain
object writer {
import assemblyIR._
def writeTo(asmList: Chain[AsmLine], printStream: PrintStream): Unit = {
asmList.iterator.foreach(printStream.println)
}
}

View File

@@ -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}):")
}

View File

@@ -39,6 +39,17 @@ val errConfig = new ErrorConfig {
)
}
object lexer {
val escapedChars: Map[String, Int] = Map(
"0" -> '\u0000',
"b" -> '\b',
"t" -> '\t',
"n" -> '\n',
"f" -> '\f',
"r" -> '\r',
"\\" -> '\\',
"'" -> '\'',
"\"" -> '\"'
)
/** Language description for the WACC lexer
*/
@@ -63,15 +74,9 @@ object lexer {
textDesc = TextDesc.plain.copy(
graphicCharacter = Basic(c => c >= ' ' && c != '\\' && c != '\'' && c != '"'),
escapeSequences = EscapeDesc.plain.copy(
literals = Set('\\', '"', '\''),
mapping = Map(
"0" -> '\u0000',
"b" -> '\b',
"t" -> '\t',
"n" -> '\n',
"f" -> '\f',
"r" -> '\r'
)
literals =
escapedChars.filter { (s, chr) => chr.toChar.toString == s }.map(_._2.toChar).toSet,
mapping = escapedChars.filter { (s, chr) => chr.toChar.toString != s }
)
),
numericDesc = NumericDesc.plain.copy(

View File

@@ -0,0 +1,89 @@
package wacc
object microWacc {
import wacc.types._
sealed trait CallTarget(val retTy: SemType)
sealed trait Expr(val ty: SemType)
sealed trait LValue extends Expr
// Atomic expressions
case class IntLiter(v: Int) extends Expr(KnownType.Int)
case class BoolLiter(v: Boolean) extends Expr(KnownType.Bool)
case class CharLiter(v: Char) extends Expr(KnownType.Char)
case class ArrayLiter(elems: List[Expr])(ty: SemType) extends Expr(ty)
case class NullLiter()(ty: SemType) extends Expr(ty)
case class Ident(name: String, uid: Int)(identTy: SemType)
extends Expr(identTy)
with CallTarget(identTy)
with LValue
case class ArrayElem(value: LValue, index: Expr)(ty: SemType) extends Expr(ty) with LValue
// Operators
case class UnaryOp(x: Expr, op: UnaryOperator)(ty: SemType) extends Expr(ty)
enum UnaryOperator {
case Negate
case Not
case Len
case Ord
case Chr
}
case class BinaryOp(x: Expr, y: Expr, op: BinaryOperator)(ty: SemType) extends Expr(ty)
enum BinaryOperator {
case Add
case Sub
case Mul
case Div
case Mod
case Greater
case GreaterEq
case Less
case LessEq
case Eq
case Neq
case And
case Or
}
object BinaryOperator {
def fromAst(op: ast.BinaryOp): BinaryOperator = op match {
case _: ast.Add => Add
case _: ast.Sub => Sub
case _: ast.Mul => Mul
case _: ast.Div => Div
case _: ast.Mod => Mod
case _: ast.Greater => Greater
case _: ast.GreaterEq => GreaterEq
case _: ast.Less => Less
case _: ast.LessEq => LessEq
case _: ast.Eq => Eq
case _: ast.Neq => Neq
case _: ast.And => And
case _: ast.Or => Or
}
}
// Statements
sealed trait Stmt
case class Builtin(val name: String)(retTy: SemType) extends CallTarget(retTy) {
override def toString(): String = name
}
object Builtin {
object Read extends Builtin("read")(?)
object Printf extends Builtin("printf")(?)
object Exit extends Builtin("exit")(?)
object Free extends Builtin("free")(?)
object Malloc extends Builtin("malloc")(?)
object PrintCharArray extends Builtin("printCharArray")(?)
}
case class Assign(lhs: LValue, rhs: Expr) extends Stmt
case class If(cond: Expr, thenBranch: List[Stmt], elseBranch: List[Stmt]) extends Stmt
case class While(cond: Expr, body: List[Stmt]) extends Stmt
case class Call(target: CallTarget, args: List[Expr]) extends Stmt with Expr(target.retTy)
case class Return(expr: Expr) extends Stmt
// Program
case class FuncDecl(name: Ident, params: List[Ident], body: List[Stmt])
case class Program(funcs: List[FuncDecl], stmts: List[Stmt])
}

View File

@@ -0,0 +1,482 @@
package wacc
import cats.syntax.all._
import scala.collection.mutable
import cats.data.NonEmptyList
object typeChecker {
import wacc.types._
case class TypeCheckerCtx(
globalNames: Map[ast.Ident, SemType],
globalFuncs: Map[ast.Ident, FuncType],
errors: mutable.Builder[Error, List[Error]]
) {
def typeOf(ident: ast.Ident): SemType = globalNames(ident)
def funcType(ident: ast.Ident): FuncType = globalFuncs(ident)
def error(err: Error): SemType =
errors += err
?
}
private enum Constraint {
case Unconstrained
// Allows weakening in one direction
case Is(ty: SemType, msg: String)
// Allows weakening in both directions, useful for array literals
case IsSymmetricCompatible(ty: SemType, msg: String)
// Does not allow weakening
case IsUnweakenable(ty: SemType, msg: String)
case IsEither(ty1: SemType, ty2: SemType, msg: String)
case Never(msg: String)
}
extension (ty: SemType) {
/** Check if a type satisfies a constraint.
*
* @param constraint
* Constraint to satisfy.
* @param pos
* Position to pass to the error, if constraint was not satisfied.
* @return
* The type if the constraint was satisfied, or ? if it was not.
*/
private def satisfies(constraint: Constraint, pos: ast.Position)(using
ctx: TypeCheckerCtx
): SemType =
(ty, constraint) match {
case (KnownType.Array(KnownType.Char), Constraint.Is(KnownType.String, _)) =>
KnownType.String
case (
KnownType.String,
Constraint.IsSymmetricCompatible(KnownType.Array(KnownType.Char), _)
) =>
KnownType.String
case (ty, Constraint.IsSymmetricCompatible(ty2, msg)) =>
ty.satisfies(Constraint.Is(ty2, msg), pos)
// Change to IsUnweakenable to disallow recursive weakening
case (ty, Constraint.Is(ty2, msg)) => ty.satisfies(Constraint.IsUnweakenable(ty2, msg), pos)
case (ty, Constraint.Unconstrained) => ty
case (ty, Constraint.Never(msg)) =>
ctx.error(Error.SemanticError(pos, msg))
case (ty, Constraint.IsEither(ty1, ty2, msg)) =>
(ty moreSpecific ty1).orElse(ty moreSpecific ty2).getOrElse {
ctx.error(Error.TypeMismatch(pos, ty1, ty, msg))
}
case (ty, Constraint.IsUnweakenable(ty2, msg)) =>
(ty moreSpecific ty2).getOrElse {
ctx.error(Error.TypeMismatch(pos, ty2, ty, msg))
}
}
/** Tries to merge two types, returning the more specific one if possible.
*
* @param ty2
* The other type to merge with.
* @return
* The more specific type if it could be determined, or None if the types are incompatible.
*/
private infix def moreSpecific(ty2: SemType): Option[SemType] =
(ty, ty2) match {
case (ty, ?) => Some(ty)
case (?, ty) => Some(ty)
case (ty1, ty2) if ty1 == ty2 => Some(ty1)
case (KnownType.Array(inn1), KnownType.Array(inn2)) =>
(inn1 moreSpecific inn2).map(KnownType.Array(_))
case (KnownType.Pair(fst1, snd1), KnownType.Pair(fst2, snd2)) =>
(fst1 moreSpecific fst2, snd1 moreSpecific snd2).mapN(KnownType.Pair(_, _))
case _ => None
}
}
/** Type-check a WACC program.
*
* @param prog
* The AST of the program to type-check.
* @param ctx
* The type checker context which includes the global names and functions, and an errors
* builder.
*/
def check(prog: ast.Program)(using
ctx: TypeCheckerCtx
): microWacc.Program =
microWacc.Program(
// Ignore function syntax types for return value and params, since those have been converted
// to SemTypes by the renamer.
prog.funcs.map { case ast.FuncDecl(_, name, params, stmts) =>
val FuncType(retType, paramTypes) = ctx.funcType(name)
microWacc.FuncDecl(
microWacc.Ident(name.v, name.uid)(retType),
params.zip(paramTypes).map { case (ast.Param(_, ident), ty) =>
microWacc.Ident(ident.v, ident.uid)(ty)
},
stmts.toList
.flatMap(
checkStmt(_, Constraint.Is(retType, s"function ${name.v} must return $retType"))
)
)
},
prog.main.toList
.flatMap(checkStmt(_, Constraint.Never("main function must not return")))
)
/** Type-check an AST statement node.
*
* @param stmt
* The statement to type-check.
* @param returnConstraint
* The constraint that any `return <expr>` statements must satisfy.
*/
private def checkStmt(stmt: ast.Stmt, returnConstraint: Constraint)(using
ctx: TypeCheckerCtx
): List[microWacc.Stmt] = stmt match {
// Ignore the type of the variable, since it has been converted to a SemType by the renamer.
case ast.VarDecl(_, name, value) =>
val expectedTy = ctx.typeOf(name)
val typedValue = checkValue(
value,
Constraint.Is(
expectedTy,
s"variable ${name.v} must be assigned a value of type $expectedTy"
)
)
List(microWacc.Assign(microWacc.Ident(name.v, name.uid)(expectedTy), typedValue))
case ast.Assign(lhs, rhs) =>
val lhsTyped = checkLValue(lhs, Constraint.Unconstrained)
val rhsTyped =
checkValue(rhs, Constraint.Is(lhsTyped.ty, s"assignment must have type ${lhsTyped.ty}"))
(lhsTyped.ty, rhsTyped.ty) match {
case (?, ?) =>
ctx.error(
Error.SemanticError(lhs.pos, "assignment with both sides of unknown type is illegal")
)
case _ => ()
}
List(microWacc.Assign(lhsTyped, rhsTyped))
case ast.Read(dest) =>
val destTyped = checkLValue(dest, Constraint.Unconstrained)
val destTy = destTyped.ty match {
case ? =>
ctx.error(
Error.SemanticError(dest.pos, "cannot read into a destination with an unknown type")
)
?
case destTy =>
destTy.satisfies(
Constraint.IsEither(
KnownType.Int,
KnownType.Char,
"read must be applied to an int or char"
),
dest.pos
)
}
List(
microWacc.Assign(
destTyped,
microWacc.Call(
microWacc.Builtin.Read,
List(
destTy match {
case KnownType.Int => " %d".toMicroWaccCharArray
case KnownType.Char | _ => " %c".toMicroWaccCharArray
},
destTyped
)
)
)
)
case ast.Free(lhs) =>
List(
microWacc.Call(
microWacc.Builtin.Free,
List(
checkValue(
lhs,
Constraint.IsEither(
KnownType.Array(?),
KnownType.Pair(?, ?),
"free must be applied to an array or pair"
)
)
)
)
)
case ast.Return(expr) =>
List(microWacc.Return(checkValue(expr, returnConstraint)))
case ast.Exit(expr) =>
List(
microWacc.Call(
microWacc.Builtin.Exit,
List(checkValue(expr, Constraint.Is(KnownType.Int, "exit value must be int")))
)
)
case ast.Print(expr, newline) =>
// This constraint should never fail, the scope-checker should have caught it already
val exprTyped = checkValue(expr, Constraint.Unconstrained)
val exprFormat = exprTyped.ty match {
case KnownType.Bool | KnownType.String => "%s"
case KnownType.Array(KnownType.Char) => "%.*s"
case KnownType.Char => "%c"
case KnownType.Int => "%d"
case KnownType.Pair(_, _) | KnownType.Array(_) | ? => "%p"
}
val printfCall = { (func: microWacc.Builtin, value: microWacc.Expr) =>
List(
microWacc.Call(
func,
List(
s"$exprFormat${if newline then "\n" else ""}".toMicroWaccCharArray,
value
)
)
)
}
exprTyped.ty match {
case KnownType.Bool =>
List(
microWacc.If(
exprTyped,
printfCall(microWacc.Builtin.Printf, "true".toMicroWaccCharArray),
printfCall(microWacc.Builtin.Printf, "false".toMicroWaccCharArray)
)
)
case KnownType.Array(KnownType.Char) =>
printfCall(microWacc.Builtin.PrintCharArray, exprTyped)
case _ => printfCall(microWacc.Builtin.Printf, exprTyped)
}
case ast.If(cond, thenStmt, elseStmt) =>
List(
microWacc.If(
checkValue(cond, Constraint.Is(KnownType.Bool, "if condition must be a bool")),
thenStmt.toList.flatMap(checkStmt(_, returnConstraint)),
elseStmt.toList.flatMap(checkStmt(_, returnConstraint))
)
)
case ast.While(cond, body) =>
List(
microWacc.While(
checkValue(cond, Constraint.Is(KnownType.Bool, "while condition must be a bool")),
body.toList.flatMap(checkStmt(_, returnConstraint))
)
)
case ast.Block(body) => body.toList.flatMap(checkStmt(_, returnConstraint))
case skip @ ast.Skip() => List.empty
}
/** Type-check an AST LValue, RValue or Expr node. This function does all 3 since these traits
* overlap in the AST.
*
* @param value
* The value to type-check.
* @param constraint
* The type constraint that the value must satisfy.
* @return
* The most specific type of the value if it could be determined, or ? if it could not.
*/
private def checkValue(value: ast.LValue | ast.RValue | ast.Expr, constraint: Constraint)(using
ctx: TypeCheckerCtx
): microWacc.Expr = value match {
case l @ ast.IntLiter(v) =>
KnownType.Int.satisfies(constraint, l.pos)
microWacc.IntLiter(v)
case l @ ast.BoolLiter(v) =>
KnownType.Bool.satisfies(constraint, l.pos)
microWacc.BoolLiter(v)
case l @ ast.CharLiter(v) =>
KnownType.Char.satisfies(constraint, l.pos)
microWacc.CharLiter(v)
case l @ ast.StrLiter(v) =>
KnownType.String.satisfies(constraint, l.pos)
v.toMicroWaccCharArray
case l @ ast.PairLiter() =>
microWacc.NullLiter()(KnownType.Pair(?, ?).satisfies(constraint, l.pos))
case ast.Parens(expr) => checkValue(expr, constraint)
case l @ ast.ArrayLiter(elems) =>
val (elemTy, elemsTyped) = elems.mapAccumulate[SemType, microWacc.Expr](?) {
case (acc, elem) =>
val elemTyped = checkValue(
elem,
Constraint.IsSymmetricCompatible(acc, s"array elements must have the same type")
)
(elemTyped.ty, elemTyped)
}
val arrayTy = KnownType
// Start with an unknown param type, make it more specific while checking the elements.
.Array(elemTy)
.satisfies(constraint, l.pos)
microWacc.ArrayLiter(elemsTyped)(arrayTy)
case l @ ast.NewPair(fst, snd) =>
val fstTyped = checkValue(fst, Constraint.Unconstrained)
val sndTyped = checkValue(snd, Constraint.Unconstrained)
microWacc.ArrayLiter(List(fstTyped, sndTyped))(
KnownType.Pair(fstTyped.ty, sndTyped.ty).satisfies(constraint, l.pos)
)
case ast.Call(id, args) =>
val funcTy @ FuncType(retTy, paramTys) = ctx.funcType(id)
if (args.length != paramTys.length) {
ctx.error(Error.FunctionParamsMismatch(id, paramTys.length, args.length, funcTy))
}
// Even if the number of arguments is wrong, we still check the types of the arguments
// in the best way we can (by taking a zip).
val argsTyped = args.zip(paramTys).map { case (arg, paramTy) =>
checkValue(arg, Constraint.Is(paramTy, s"argument type mismatch in function ${id.v}"))
}
microWacc.Call(microWacc.Ident(id.v, id.uid)(retTy.satisfies(constraint, id.pos)), argsTyped)
// Unary operators
case ast.Negate(x) =>
microWacc.UnaryOp(
checkValue(x, Constraint.Is(KnownType.Int, "negation must be applied to an int")),
microWacc.UnaryOperator.Negate
)(KnownType.Int.satisfies(constraint, x.pos))
case ast.Not(x) =>
microWacc.UnaryOp(
checkValue(x, Constraint.Is(KnownType.Bool, "logical not must be applied to a bool")),
microWacc.UnaryOperator.Not
)(KnownType.Bool.satisfies(constraint, x.pos))
case ast.Len(x) =>
microWacc.UnaryOp(
checkValue(x, Constraint.Is(KnownType.Array(?), "len must be applied to an array")),
microWacc.UnaryOperator.Len
)(KnownType.Int.satisfies(constraint, x.pos))
case ast.Ord(x) =>
microWacc.UnaryOp(
checkValue(x, Constraint.Is(KnownType.Char, "ord must be applied to a char")),
microWacc.UnaryOperator.Ord
)(KnownType.Int.satisfies(constraint, x.pos))
case ast.Chr(x) =>
microWacc.UnaryOp(
checkValue(x, Constraint.Is(KnownType.Int, "chr must be applied to an int")),
microWacc.UnaryOperator.Chr
)(KnownType.Char.satisfies(constraint, x.pos))
// Binary operators
case op: (ast.Add | ast.Sub | ast.Mul | ast.Div | ast.Mod) =>
val operand = Constraint.Is(KnownType.Int, s"${op.name} operator must be applied to an int")
microWacc.BinaryOp(
checkValue(op.x, operand),
checkValue(op.y, operand),
microWacc.BinaryOperator.fromAst(op)
)(KnownType.Int.satisfies(constraint, op.pos))
case op: (ast.Eq | ast.Neq) =>
val xTyped = checkValue(op.x, Constraint.Unconstrained)
microWacc.BinaryOp(
xTyped,
checkValue(
op.y,
Constraint
.Is(xTyped.ty, s"${op.name} operator must be applied to values of the same type")
),
microWacc.BinaryOperator.fromAst(op)
)(KnownType.Bool.satisfies(constraint, op.pos))
case op: (ast.Less | ast.LessEq | ast.Greater | ast.GreaterEq) =>
val xConstraint = Constraint.IsEither(
KnownType.Int,
KnownType.Char,
s"${op.name} operator must be applied to an int or char"
)
val xTyped = checkValue(op.x, xConstraint)
// If x type-check failed, we still want to check y is an Int or Char (rather than ?)
val yConstraint = xTyped.ty match {
case ? => xConstraint
case xTy =>
Constraint.Is(xTy, s"${op.name} operator must be applied to values of the same type")
}
microWacc.BinaryOp(
xTyped,
checkValue(op.y, yConstraint),
microWacc.BinaryOperator.fromAst(op)
)(KnownType.Bool.satisfies(constraint, op.pos))
case op: (ast.And | ast.Or) =>
val operand = Constraint.Is(KnownType.Bool, s"${op.name} operator must be applied to a bool")
microWacc.BinaryOp(
checkValue(op.x, operand),
checkValue(op.y, operand),
microWacc.BinaryOperator.fromAst(op)
)(KnownType.Bool.satisfies(constraint, op.pos))
case lvalue: ast.LValue => checkLValue(lvalue, constraint)
}
/** Type-check an AST LValue node. Separate because microWacc keeps LValues
*
* @param value
* The value to type-check.
* @param constraint
* The type constraint that the value must satisfy.
* @param ctx
* The type checker context which includes the global names and functions, and an errors
* builder.
* @return
* The most specific type of the value if it could be determined, or ? if it could not.
*/
private def checkLValue(value: ast.LValue, constraint: Constraint)(using
ctx: TypeCheckerCtx
): microWacc.LValue = value match {
case id @ ast.Ident(name, uid) =>
microWacc.Ident(name, uid)(ctx.typeOf(id).satisfies(constraint, id.pos))
case ast.ArrayElem(id, indices) =>
val arrayTy = ctx.typeOf(id)
val (elemTy, indicesTyped) = indices.mapAccumulate(arrayTy) { (acc, elem) =>
val idxTyped = checkValue(elem, Constraint.Is(KnownType.Int, "array index must be an int"))
val next = acc match {
case KnownType.Array(innerTy) => innerTy
case ? => ? // we can keep indexing an unknown type
case nonArrayTy =>
ctx.error(
Error.TypeMismatch(
elem.pos,
KnownType.Array(?),
acc,
"cannot index into a non-array"
)
)
?
}
(next, idxTyped)
}
val firstArrayElem = microWacc.ArrayElem(
microWacc.Ident(id.v, id.uid)(arrayTy),
indicesTyped.head
)(elemTy.satisfies(constraint, value.pos))
val arrayElem = indicesTyped.tail.foldLeft(firstArrayElem) { (acc, idx) =>
microWacc.ArrayElem(acc, idx)(KnownType.Array(acc.ty))
}
// Need to type-check the final arrayElem with the constraint
microWacc.ArrayElem(arrayElem.value, arrayElem.index)(elemTy.satisfies(constraint, value.pos))
case ast.Fst(elem) =>
val elemTyped = checkLValue(
elem,
Constraint.Is(KnownType.Pair(?, ?), "fst must be applied to a pair")
)
microWacc.ArrayElem(
elemTyped,
microWacc.IntLiter(0)
)(elemTyped.ty match {
case KnownType.Pair(left, _) =>
left.satisfies(constraint, elem.pos)
case _ => ctx.error(Error.InternalError(elem.pos, "fst must be applied to a pair"))
})
case ast.Snd(elem) =>
val elemTyped = checkLValue(
elem,
Constraint.Is(KnownType.Pair(?, ?), "snd must be applied to a pair")
)
microWacc.ArrayElem(
elemTyped,
microWacc.IntLiter(1)
)(elemTyped.ty match {
case KnownType.Pair(_, right) =>
right.satisfies(constraint, elem.pos)
case _ => ctx.error(Error.InternalError(elem.pos, "snd must be applied to a pair"))
})
}
extension (s: String) {
def toMicroWaccCharArray: microWacc.ArrayLiter =
microWacc.ArrayLiter(s.map(microWacc.CharLiter(_)).toList)(KnownType.String)
}
}

View File

@@ -1,322 +0,0 @@
package wacc
import cats.syntax.all._
import scala.collection.mutable
object typeChecker {
import wacc.ast._
import wacc.types._
case class TypeCheckerCtx(
globalNames: Map[Ident, SemType],
globalFuncs: Map[Ident, FuncType],
errors: mutable.Builder[Error, List[Error]]
) {
def typeOf(ident: Ident): SemType = globalNames(ident)
def funcType(ident: Ident): FuncType = globalFuncs(ident)
def error(err: Error): SemType =
errors += err
?
}
private enum Constraint {
case Unconstrained
// Allows weakening in one direction
case Is(ty: SemType, msg: String)
// Allows weakening in both directions, useful for array literals
case IsSymmetricCompatible(ty: SemType, msg: String)
// Does not allow weakening
case IsUnweakenable(ty: SemType, msg: String)
case IsEither(ty1: SemType, ty2: SemType, msg: String)
case Never(msg: String)
}
extension (ty: SemType) {
/** Check if a type satisfies a constraint.
*
* @param constraint
* Constraint to satisfy.
* @param pos
* Position to pass to the error, if constraint was not satisfied.
* @return
* The type if the constraint was satisfied, or ? if it was not.
*/
private def satisfies(constraint: Constraint, pos: Position)(using
ctx: TypeCheckerCtx
): SemType =
(ty, constraint) match {
case (KnownType.Array(KnownType.Char), Constraint.Is(KnownType.String, _)) =>
KnownType.String
case (
KnownType.String,
Constraint.IsSymmetricCompatible(KnownType.Array(KnownType.Char), _)
) =>
KnownType.String
case (ty, Constraint.IsSymmetricCompatible(ty2, msg)) =>
ty.satisfies(Constraint.Is(ty2, msg), pos)
// Change to IsUnweakenable to disallow recursive weakening
case (ty, Constraint.Is(ty2, msg)) => ty.satisfies(Constraint.IsUnweakenable(ty2, msg), pos)
case (ty, Constraint.Unconstrained) => ty
case (ty, Constraint.Never(msg)) =>
ctx.error(Error.SemanticError(pos, msg))
case (ty, Constraint.IsEither(ty1, ty2, msg)) =>
(ty moreSpecific ty1).orElse(ty moreSpecific ty2).getOrElse {
ctx.error(Error.TypeMismatch(pos, ty1, ty, msg))
}
case (ty, Constraint.IsUnweakenable(ty2, msg)) =>
(ty moreSpecific ty2).getOrElse {
ctx.error(Error.TypeMismatch(pos, ty2, ty, msg))
}
}
/** Tries to merge two types, returning the more specific one if possible.
*
* @param ty2
* The other type to merge with.
* @return
* The more specific type if it could be determined, or None if the types are incompatible.
*/
private infix def moreSpecific(ty2: SemType): Option[SemType] =
(ty, ty2) match {
case (ty, ?) => Some(ty)
case (?, ty) => Some(ty)
case (ty1, ty2) if ty1 == ty2 => Some(ty1)
case (KnownType.Array(inn1), KnownType.Array(inn2)) =>
(inn1 moreSpecific inn2).map(KnownType.Array(_))
case (KnownType.Pair(fst1, snd1), KnownType.Pair(fst2, snd2)) =>
(fst1 moreSpecific fst2, snd1 moreSpecific snd2).mapN(KnownType.Pair(_, _))
case _ => None
}
}
/** Type-check a WACC program.
*
* @param prog
* The AST of the program to type-check.
* @param ctx
* The type checker context which includes the global names and functions, and an errors
* builder.
*/
def check(prog: Program)(using
ctx: TypeCheckerCtx
): Unit = {
// Ignore function syntax types for return value and params, since those have been converted
// to SemTypes by the renamer.
prog.funcs.foreach { case FuncDecl(_, name, _, stmts) =>
val FuncType(retType, _) = ctx.funcType(name)
stmts.toList.foreach(
checkStmt(_, Constraint.Is(retType, s"function ${name.v} must return $retType"))
)
}
prog.main.toList.foreach(checkStmt(_, Constraint.Never("main function must not return")))
}
/** Type-check an AST statement node.
*
* @param stmt
* The statement to type-check.
* @param returnConstraint
* The constraint that any `return <expr>` statements must satisfy.
*/
private def checkStmt(stmt: Stmt, returnConstraint: Constraint)(using
ctx: TypeCheckerCtx
): Unit = stmt match {
// Ignore the type of the variable, since it has been converted to a SemType by the renamer.
case VarDecl(_, name, value) =>
val expectedTy = ctx.typeOf(name)
checkValue(
value,
Constraint.Is(
expectedTy,
s"variable ${name.v} must be assigned a value of type $expectedTy"
)
)
case Assign(lhs, rhs) =>
val lhsTy = checkValue(lhs, Constraint.Unconstrained)
(lhsTy, checkValue(rhs, Constraint.Is(lhsTy, s"assignment must have type $lhsTy"))) match {
case (?, ?) =>
ctx.error(
Error.SemanticError(lhs.pos, "assignment with both sides of unknown type is illegal")
)
case _ => ()
}
case Read(dest) =>
checkValue(dest, Constraint.Unconstrained) match {
case ? =>
ctx.error(
Error.SemanticError(dest.pos, "cannot read into a destination with an unknown type")
)
case destTy =>
destTy.satisfies(
Constraint.IsEither(
KnownType.Int,
KnownType.Char,
"read must be applied to an int or char"
),
dest.pos
)
}
case Free(lhs) =>
checkValue(
lhs,
Constraint.IsEither(
KnownType.Array(?),
KnownType.Pair(?, ?),
"free must be applied to an array or pair"
)
)
case Return(expr) =>
checkValue(expr, returnConstraint)
case Exit(expr) =>
checkValue(expr, Constraint.Is(KnownType.Int, "exit value must be int"))
case Print(expr, _) =>
// This constraint should never fail, the scope-checker should have caught it already
checkValue(expr, Constraint.Unconstrained)
case If(cond, thenStmt, elseStmt) =>
checkValue(cond, Constraint.Is(KnownType.Bool, "if condition must be a bool"))
thenStmt.toList.foreach(checkStmt(_, returnConstraint))
elseStmt.toList.foreach(checkStmt(_, returnConstraint))
case While(cond, body) =>
checkValue(cond, Constraint.Is(KnownType.Bool, "while condition must be a bool"))
body.toList.foreach(checkStmt(_, returnConstraint))
case Block(body) =>
body.toList.foreach(checkStmt(_, returnConstraint))
case Skip() => ()
}
/** Type-check an AST LValue, RValue or Expr node. This function does all 3 since these traits
* overlap in the AST.
*
* @param value
* The value to type-check.
* @param constraint
* The type constraint that the value must satisfy.
* @return
* The most specific type of the value if it could be determined, or ? if it could not.
*/
private def checkValue(value: LValue | RValue | Expr, constraint: Constraint)(using
ctx: TypeCheckerCtx
): SemType = value match {
case l @ IntLiter(_) => KnownType.Int.satisfies(constraint, l.pos)
case l @ BoolLiter(_) => KnownType.Bool.satisfies(constraint, l.pos)
case l @ CharLiter(_) => KnownType.Char.satisfies(constraint, l.pos)
case l @ StrLiter(_) => KnownType.String.satisfies(constraint, l.pos)
case l @ PairLiter() => KnownType.Pair(?, ?).satisfies(constraint, l.pos)
case id: Ident =>
ctx.typeOf(id).satisfies(constraint, id.pos)
case ArrayElem(id, indices) =>
val arrayTy = ctx.typeOf(id)
val elemTy = indices.foldLeftM(arrayTy) { (acc, elem) =>
checkValue(elem, Constraint.Is(KnownType.Int, "array index must be an int"))
acc match {
case KnownType.Array(innerTy) => Some(innerTy)
case ? => Some(?) // we can keep indexing an unknown type
case nonArrayTy =>
ctx.error(
Error.TypeMismatch(elem.pos, KnownType.Array(?), acc, "cannot index into a non-array")
)
None
}
}
elemTy.getOrElse(?).satisfies(constraint, id.pos)
case Parens(expr) => checkValue(expr, constraint)
case l @ ArrayLiter(elems) =>
KnownType
// Start with an unknown param type, make it more specific while checking the elements.
.Array(elems.foldLeft[SemType](?) { case (acc, elem) =>
checkValue(
elem,
Constraint.IsSymmetricCompatible(acc, s"array elements must have the same type")
)
})
.satisfies(constraint, l.pos)
case l @ NewPair(fst, snd) =>
KnownType
.Pair(
checkValue(fst, Constraint.Unconstrained),
checkValue(snd, Constraint.Unconstrained)
)
.satisfies(constraint, l.pos)
case Call(id, args) =>
val funcTy @ FuncType(retTy, paramTys) = ctx.funcType(id)
if (args.length != paramTys.length) {
ctx.error(Error.FunctionParamsMismatch(id, paramTys.length, args.length, funcTy))
}
// Even if the number of arguments is wrong, we still check the types of the arguments
// in the best way we can (by taking a zip).
args.zip(paramTys).foreach { case (arg, paramTy) =>
checkValue(arg, Constraint.Is(paramTy, s"argument type mismatch in function ${id.v}"))
}
retTy.satisfies(constraint, id.pos)
case Fst(elem) =>
checkValue(
elem,
Constraint.Is(KnownType.Pair(?, ?), "fst must be applied to a pair")
) match {
case what @ KnownType.Pair(left, _) =>
left.satisfies(constraint, elem.pos)
case _ => ctx.error(Error.InternalError(elem.pos, "fst must be applied to a pair"))
}
case Snd(elem) =>
checkValue(
elem,
Constraint.Is(KnownType.Pair(?, ?), "snd must be applied to a pair")
) match {
case KnownType.Pair(_, right) => right.satisfies(constraint, elem.pos)
case _ => ctx.error(Error.InternalError(elem.pos, "snd must be applied to a pair"))
}
// Unary operators
case Negate(x) =>
checkValue(x, Constraint.Is(KnownType.Int, "negation must be applied to an int"))
KnownType.Int.satisfies(constraint, x.pos)
case Not(x) =>
checkValue(x, Constraint.Is(KnownType.Bool, "logical not must be applied to a bool"))
KnownType.Bool.satisfies(constraint, x.pos)
case Len(x) =>
checkValue(x, Constraint.Is(KnownType.Array(?), "len must be applied to an array"))
KnownType.Int.satisfies(constraint, x.pos)
case Ord(x) =>
checkValue(x, Constraint.Is(KnownType.Char, "ord must be applied to a char"))
KnownType.Int.satisfies(constraint, x.pos)
case Chr(x) =>
checkValue(x, Constraint.Is(KnownType.Int, "chr must be applied to an int"))
KnownType.Char.satisfies(constraint, x.pos)
// Binary operators
case op: (Add | Sub | Mul | Div | Mod) =>
val operand = Constraint.Is(KnownType.Int, s"${op.name} operator must be applied to an int")
checkValue(op.x, operand)
checkValue(op.y, operand)
KnownType.Int.satisfies(constraint, op.pos)
case op: (Eq | Neq) =>
val xTy = checkValue(op.x, Constraint.Unconstrained)
checkValue(
op.y,
Constraint.Is(xTy, s"${op.name} operator must be applied to values of the same type")
)
KnownType.Bool.satisfies(constraint, op.pos)
case op: (Less | LessEq | Greater | GreaterEq) =>
val xConstraint = Constraint.IsEither(
KnownType.Int,
KnownType.Char,
s"${op.name} operator must be applied to an int or char"
)
// If x type-check failed, we still want to check y is an Int or Char (rather than ?)
val yConstraint = checkValue(op.x, xConstraint) match {
case ? => xConstraint
case xTy =>
Constraint.Is(xTy, s"${op.name} operator must be applied to values of the same type")
}
checkValue(op.y, yConstraint)
KnownType.Bool.satisfies(constraint, op.pos)
case op: (And | Or) =>
val operand = Constraint.Is(KnownType.Bool, s"${op.name} operator must be applied to a bool")
checkValue(op.x, operand)
checkValue(op.y, operand)
KnownType.Bool.satisfies(constraint, op.pos)
}
}

View File

@@ -1,10 +1,14 @@
package wacc
import org.scalatest.{ParallelTestExecution, BeforeAndAfterAll}
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.Inspectors.forEvery
import java.io.File
import sys.process._
import java.io.PrintStream
import scala.io.Source
class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll with ParallelTestExecution {
class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll {
val files =
allWaccFiles("wacc-examples/valid").map { p =>
(p.toString, List(0))
@@ -20,12 +24,65 @@ 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"))
s"$filename" should "be compiled with correct result" in {
val result = compile(filename)
assert(expectedResult.contains(result))
}
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
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 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)
assert(
stdout.toString
.replaceAll("0x[0-9a-f]+", "#addrs#")
.replaceAll("fatal error:.*", "#runtime_error#\u0000")
.takeWhile(_ != '\u0000')
== expectedOutput
)
}
}
@@ -33,57 +90,28 @@ 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/exit.*$",
// "^.*wacc-examples/valid/basic/skip.*$",
// "^.*wacc-examples/valid/expressions.*$",
// "^.*wacc-examples/valid/function/nested_functions.*$",
// "^.*wacc-examples/valid/function/simple_functions.*$",
// "^.*wacc-examples/valid/if.*$",
// "^.*wacc-examples/valid/IO/print.*$",
// "^.*wacc-examples/valid/IO/read.*$",
// "^.*wacc-examples/valid/IO/IOLoop.wacc.*$",
// "^.*wacc-examples/valid/IO/IOSequence.wacc.*$",
// "^.*wacc-examples/valid/pairs.*$",
//"^.*wacc-examples/valid/runtimeErr.*$",
// "^.*wacc-examples/valid/scope.*$",
// "^.*wacc-examples/valid/sequence.*$",
// "^.*wacc-examples/valid/variables.*$",
// "^.*wacc-examples/valid/while.*$",
// format: on
// format: on
).find(filename.contains).isDefined
).find(filename.matches).isDefined
}

View File

@@ -0,0 +1,68 @@
import org.scalatest.funsuite.AnyFunSuite
import wacc.assemblyIR._
import wacc.assemblyIR.Size._
import wacc.assemblyIR.RegName._
class instructionSpec extends AnyFunSuite {
val named64BitRegister = Register(Q64, AX)
test("named 64-bit register toString") {
assert(named64BitRegister.toString == "rax")
}
val named32BitRegister = Register(D32, AX)
test("named 32-bit register toString") {
assert(named32BitRegister.toString == "eax")
}
val scratch64BitRegister = Register(Q64, R8)
test("scratch 64-bit register toString") {
assert(scratch64BitRegister.toString == "r8")
}
val scratch32BitRegister = Register(D32, R8)
test("scratch 32-bit register toString") {
assert(scratch32BitRegister.toString == "r8d")
}
val memLocationWithRegister = MemLocation(named64BitRegister, opSize = Some(Q64))
test("mem location with register toString") {
assert(memLocationWithRegister.toString == "qword ptr [rax]")
}
val memLocationFull =
MemLocation(named64BitRegister, 32, (scratch64BitRegister, 10), Some(B8))
test("mem location with all fields toString") {
assert(memLocationFull.toString == "byte ptr [rax + r8 * 10 + 32]")
}
val immediateVal = ImmediateVal(123)
test("immediate value toString") {
assert(immediateVal.toString == "123")
}
val addInstruction = Add(named64BitRegister, immediateVal)
test("x86: add instruction toString") {
assert(addInstruction.toString == "\tadd rax, 123")
}
val subInstruction = Subtract(scratch64BitRegister, named64BitRegister)
test("x86: sub instruction toString") {
assert(subInstruction.toString == "\tsub r8, rax")
}
val callInstruction = Call(CLibFunc.Scanf)
test("x86: call instruction toString") {
assert(callInstruction.toString == "\tcall scanf@plt")
}
}

1
wacc.target Normal file
View File

@@ -0,0 +1 @@
x86-64