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>
This commit is contained in:
Teixeira, Jonny
2025-02-28 00:23:12 +00:00
5 changed files with 184 additions and 58 deletions

View File

@@ -82,9 +82,11 @@ def compile(filename: String, outFile: Option[File] = None)(using
def main(args: Array[String]): Unit = def main(args: Array[String]): Unit =
OParser.parse(cliParser, args, CliConfig()) match { OParser.parse(cliParser, args, CliConfig()) match {
case Some(config) => case Some(config) =>
System.exit(
compile( compile(
config.file.getAbsolutePath, config.file.getAbsolutePath,
outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s")) outFile = Some(File(".", config.file.getName.stripSuffix(".wacc") + ".s"))
) )
)
case None => case None =>
} }

View File

@@ -0,0 +1,122 @@
package wacc
import cats.data.Chain
import wacc.assemblyIR._
sealed trait RuntimeError {
def strLabel: String
def errStr: String
def errLabel: String
def stringDef: Chain[AsmLine] = Chain(
Directive.Int(errStr.length),
LabelDef(strLabel),
Directive.Asciz(errStr)
)
def generateHandler: Chain[AsmLine]
}
object RuntimeError {
// TODO: Refactor to mitigate imports and redeclared vals perhaps
import wacc.asmGenerator.stackAlign
import assemblyIR.Size._
import assemblyIR.RegName._
// private val RAX = Register(Q64, AX)
// private val EAX = Register(D32, AX)
private val RDI = Register(Q64, DI)
private val RIP = Register(Q64, IP)
// private val RBP = Register(Q64, BP)
private val RSI = Register(Q64, SI)
// private val RDX = Register(Q64, DX)
// private val RCX = Register(Q64, CX)
case object ZeroDivError extends RuntimeError {
val strLabel = ".L._errDivZero_str0"
val errStr = "fatal error: division or modulo by zero"
val errLabel = ".L._errDivZero"
def generateHandler: Chain[AsmLine] = Chain(
LabelDef(ZeroDivError.errLabel),
stackAlign,
Load(RDI, IndexAddress(RIP, LabelArg(ZeroDivError.strLabel))),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(-1)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object BadChrError extends RuntimeError {
val strLabel = ".L._errBadChr_str0"
val errStr = "fatal error: int %d is not an ASCII character 0-127"
val errLabel = ".L._errBadChr"
def generateHandler: Chain[AsmLine] = Chain(
LabelDef(BadChrError.errLabel),
Pop(RSI),
stackAlign,
Load(RDI, IndexAddress(RIP, LabelArg(BadChrError.strLabel))),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(255)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object NullPtrError extends RuntimeError {
val strLabel = ".L._errNullPtr_str0"
val errStr = "fatal error: null pair dereferenced or freed"
val errLabel = ".L._errNullPtr"
def generateHandler: Chain[AsmLine] = Chain(
LabelDef(NullPtrError.errLabel),
stackAlign,
Load(RDI, IndexAddress(RIP, LabelArg(NullPtrError.strLabel))),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(255)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object OverflowError extends RuntimeError {
val strLabel = ".L._errOverflow_str0"
val errStr = "fatal error: integer overflow or underflow occurred"
val errLabel = ".L._errOverflow"
def generateHandler: Chain[AsmLine] = Chain(
LabelDef(OverflowError.errLabel),
stackAlign,
Load(RDI, IndexAddress(RIP, LabelArg(OverflowError.strLabel))),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(255)),
assemblyIR.Call(CLibFunc.Exit)
)
}
case object OutOfBoundsError extends RuntimeError {
val strLabel = ".L._errOutOfBounds_str0"
val errStr = "fatal error: array index %d out of bounds"
val errLabel = ".L._errOutOfBounds"
def generateHandler: Chain[AsmLine] = Chain(
LabelDef(OutOfBoundsError.errLabel),
Move(RSI, Register(Q64, CX)),
stackAlign,
Load(RDI, IndexAddress(RIP, LabelArg(OutOfBoundsError.strLabel))),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(255)),
assemblyIR.Call(CLibFunc.Exit)
)
}
val all: Chain[RuntimeError] =
Chain(ZeroDivError, BadChrError, NullPtrError, OverflowError, OutOfBoundsError)
}

View File

@@ -3,6 +3,7 @@ package wacc
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import cats.data.Chain import cats.data.Chain
import cats.syntax.foldable._ import cats.syntax.foldable._
import wacc.RuntimeError._
object asmGenerator { object asmGenerator {
import microWacc._ import microWacc._
@@ -13,24 +14,6 @@ object asmGenerator {
import sizeExtensions._ import sizeExtensions._
import lexer.escapedChars import lexer.escapedChars
abstract case class Error() {
def strLabel: String
def errStr: String
def errLabel: String
def stringDef: Chain[AsmLine] = Chain(
Directive.Int(errStr.size),
LabelDef(strLabel),
Directive.Asciz(errStr)
)
}
object zeroDivError extends Error {
// TODO: is this bad? Can we make an error case class/some other structure?
def strLabel = ".L._errDivZero_str0"
def errStr = "fatal error: division or modulo by zero"
def errLabel = ".L._errDivZero"
}
private val RAX = Register(Q64, AX) private val RAX = Register(Q64, AX)
private val EAX = Register(D32, AX) private val EAX = Register(D32, AX)
private val RDI = Register(Q64, DI) private val RDI = Register(Q64, DI)
@@ -39,6 +22,7 @@ object asmGenerator {
private val RSI = Register(Q64, SI) private val RSI = Register(Q64, SI)
private val RDX = Register(Q64, DX) private val RDX = Register(Q64, DX)
private val RCX = Register(Q64, CX) private val RCX = Register(Q64, CX)
private val ECX = Register(D32, CX)
private val argRegs = List(DI, SI, DX, CX, R8, R9) private val argRegs = List(DI, SI, DX, CX, R8, R9)
extension [T](chain: Chain[T]) extension [T](chain: Chain[T])
@@ -80,7 +64,7 @@ object asmGenerator {
LabelDef(s".L.str$i"), LabelDef(s".L.str$i"),
Directive.Asciz(str.escaped) Directive.Asciz(str.escaped)
) )
} ++ zeroDivError.stringDef } ++ RuntimeError.all.foldMap(_.stringDef)
Chain( Chain(
Directive.IntelSyntax, Directive.IntelSyntax,
@@ -161,7 +145,16 @@ object asmGenerator {
// Out of memory check is optional // Out of memory check is optional
) )
chain ++= wrapBuiltinFunc(labelGenerator.getLabel(Builtin.Free), Chain.empty) chain ++= wrapBuiltinFunc(
labelGenerator.getLabel(Builtin.Free),
Chain(
stackAlign,
Move(RDI, RAX),
Compare(RDI, ImmediateVal(0)),
Jump(LabelArg(NullPtrError.errLabel), Cond.Equal),
assemblyIR.Call(CLibFunc.Free)
)
)
chain ++= wrapBuiltinFunc( chain ++= wrapBuiltinFunc(
labelGenerator.getLabel(Builtin.Read), labelGenerator.getLabel(Builtin.Read),
@@ -175,16 +168,7 @@ object asmGenerator {
) )
) )
chain ++= Chain( chain ++= RuntimeError.all.foldMap(_.generateHandler)
// TODO can this be done with a call to generateStmt?
// Consider other error cases -> look to generalise
LabelDef(zeroDivError.errLabel),
stackAlign,
Load(RDI, IndexAddress(RIP, LabelArg(zeroDivError.strLabel))),
assemblyIR.Call(CLibFunc.PrintF),
Move(RDI, ImmediateVal(-1)),
assemblyIR.Call(CLibFunc.Exit)
)
chain chain
} }
@@ -207,9 +191,17 @@ object asmGenerator {
case ArrayElem(x, i) => case ArrayElem(x, i) =>
chain ++= evalExprOntoStack(rhs) chain ++= evalExprOntoStack(rhs)
chain ++= evalExprOntoStack(i) chain ++= evalExprOntoStack(i)
chain += stack.pop(RCX)
chain += Compare(ECX, ImmediateVal(0))
chain += Jump(LabelArg(OutOfBoundsError.errLabel), Cond.Less)
chain += stack.push(Q64, RCX)
chain ++= evalExprOntoStack(x) chain ++= evalExprOntoStack(x)
chain += stack.pop(RAX) chain += stack.pop(RAX)
chain += stack.pop(RCX) chain += stack.pop(RCX)
chain += Compare(EAX, ImmediateVal(0))
chain += Jump(LabelArg(NullPtrError.errLabel), Cond.Equal)
chain += Compare(MemLocation(RAX, D32), ECX)
chain += Jump(LabelArg(OutOfBoundsError.errLabel), Cond.LessEqual)
chain += stack.pop(RDX) chain += stack.pop(RDX)
chain += Move( chain += Move(
@@ -311,7 +303,13 @@ object asmGenerator {
chain ++= evalExprOntoStack(x) chain ++= evalExprOntoStack(x)
chain ++= evalExprOntoStack(i) chain ++= evalExprOntoStack(i)
chain += stack.pop(RCX) chain += stack.pop(RCX)
chain += Compare(RCX, ImmediateVal(0))
chain += Jump(LabelArg(OutOfBoundsError.errLabel), Cond.Less)
chain += stack.pop(RAX) chain += stack.pop(RAX)
chain += Compare(EAX, ImmediateVal(0))
chain += Jump(LabelArg(NullPtrError.errLabel), Cond.Equal)
chain += Compare(MemLocation(RAX, D32), ECX)
chain += Jump(LabelArg(OutOfBoundsError.errLabel), Cond.LessEqual)
// + Int because we store the length of the array at the start // + Int because we store the length of the array at the start
chain += Move( chain += Move(
Register(x.ty.elemSize, AX), Register(x.ty.elemSize, AX),
@@ -321,13 +319,22 @@ object asmGenerator {
case UnaryOp(x, op) => case UnaryOp(x, op) =>
chain ++= evalExprOntoStack(x) chain ++= evalExprOntoStack(x)
op match { op match {
case UnaryOperator.Chr | UnaryOperator.Ord => // No op needed case UnaryOperator.Chr =>
chain += Move(EAX, stack.head)
chain += And(EAX, ImmediateVal(-128))
chain += Compare(EAX, ImmediateVal(0))
chain += Jump(LabelArg(BadChrError.errLabel), Cond.NotEqual)
case UnaryOperator.Ord => // No op needed
case UnaryOperator.Len => case UnaryOperator.Len =>
chain += stack.pop(RAX) chain += stack.pop(RAX)
chain += Move(EAX, MemLocation(RAX, D32)) chain += Move(EAX, MemLocation(RAX, D32))
chain += stack.push(D32, RAX) chain += stack.push(D32, RAX)
case UnaryOperator.Negate => case UnaryOperator.Negate =>
chain += Negate(stack.head) chain += Xor(EAX, EAX)
chain += Subtract(EAX, stack.head)
chain += Jump(LabelArg(OverflowError.errLabel), Cond.Overflow)
chain += stack.drop()
chain += stack.push(Q64, RAX)
case UnaryOperator.Not => case UnaryOperator.Not =>
chain += Xor(stack.head, ImmediateVal(1)) chain += Xor(stack.head, ImmediateVal(1))
} }
@@ -341,24 +348,29 @@ object asmGenerator {
op match { op match {
case BinaryOperator.Add => case BinaryOperator.Add =>
chain += Add(stack.head, destX) chain += Add(stack.head, destX)
chain += Jump(LabelArg(OverflowError.errLabel), Cond.Overflow)
case BinaryOperator.Sub => case BinaryOperator.Sub =>
chain += Subtract(destX, stack.head) chain += Subtract(destX, stack.head)
chain += Jump(LabelArg(OverflowError.errLabel), Cond.Overflow)
chain += stack.drop() chain += stack.drop()
chain += stack.push(destX.size, RAX) chain += stack.push(destX.size, RAX)
case BinaryOperator.Mul => case BinaryOperator.Mul =>
chain += Multiply(destX, stack.head) chain += Multiply(destX, stack.head)
chain += Jump(LabelArg(OverflowError.errLabel), Cond.Overflow)
chain += stack.drop() chain += stack.drop()
chain += stack.push(destX.size, RAX) chain += stack.push(destX.size, RAX)
case BinaryOperator.Div => case BinaryOperator.Div =>
chain += Compare(stack.head, ImmediateVal(0)) chain += Compare(stack.head, ImmediateVal(0))
chain += Jump(LabelArg(zeroDivError.errLabel), Cond.Equal) chain += Jump(LabelArg(ZeroDivError.errLabel), Cond.Equal)
chain += CDQ() chain += CDQ()
chain += Divide(stack.head) chain += Divide(stack.head)
chain += stack.drop() chain += stack.drop()
chain += stack.push(destX.size, RAX) chain += stack.push(destX.size, RAX)
case BinaryOperator.Mod => case BinaryOperator.Mod =>
chain += Compare(stack.head, ImmediateVal(0))
chain += Jump(LabelArg(ZeroDivError.errLabel), Cond.Equal)
chain += CDQ() chain += CDQ()
chain += Divide(stack.head) chain += Divide(stack.head)
chain += stack.drop() chain += stack.drop()
@@ -444,7 +456,7 @@ object asmGenerator {
chain chain
} }
private def stackAlign: AsmLine = And(Register(Q64, SP), ImmediateVal(-16)) def stackAlign: AsmLine = And(Register(Q64, SP), ImmediateVal(-16))
private def zeroRest(dest: Dest, size: Size): Chain[AsmLine] = size match { private def zeroRest(dest: Dest, size: Size): Chain[AsmLine] = size match {
case Q64 | D32 => Chain.empty case Q64 | D32 => Chain.empty
case _ => Chain.one(And(dest, ImmediateVal((1 << (size.toInt * 8)) - 1))) case _ => Chain.one(And(dest, ImmediateVal((1 << (size.toInt * 8)) - 1)))

View File

@@ -41,23 +41,7 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll {
val inputLine = val inputLine =
contents contents
.find(_.matches("^# ?[Ii]nput:.*$")) .find(_.matches("^# ?[Ii]nput:.*$"))
.map(line => .map(_.split(":").last.strip + "\n")
("" :: line.split(":").last.strip.split(" ").toList)
.sliding(2)
.flatMap { arr =>
if (
// First entry has no space in front
arr(0) == "" ||
// int followed by non-digit, space can be removed
arr(0).toIntOption.nonEmpty && !arr(1)(0).isDigit ||
// non-int followed by int, space can be removed
!arr(0).last.isDigit && arr(1).toIntOption.nonEmpty
)
then List(arr(1))
else List(" ", arr(1))
}
.mkString
)
.getOrElse("") .getOrElse("")
val outputLineIdx = contents.indexWhere(_.matches("^# ?[Oo]utput:.*$")) val outputLineIdx = contents.indexWhere(_.matches("^# ?[Oo]utput:.*$"))
val expectedOutput = val expectedOutput =
@@ -92,7 +76,13 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll {
) )
assert(process.exitValue == expectedExit) assert(process.exitValue == expectedExit)
assert(stdout.toString.replaceAll("0x[0-9a-f]+", "#addrs#") == expectedOutput) assert(
stdout.toString
.replaceAll("0x[0-9a-f]+", "#addrs#")
.replaceAll("fatal error:.*", "#runtime_error#\u0000")
.takeWhile(_ != '\u0000')
== expectedOutput
)
} }
} }
@@ -117,7 +107,7 @@ class ParallelExamplesSpec extends AnyFlatSpec with BeforeAndAfterAll {
// "^.*wacc-examples/valid/IO/IOLoop.wacc.*$", // "^.*wacc-examples/valid/IO/IOLoop.wacc.*$",
// "^.*wacc-examples/valid/IO/IOSequence.wacc.*$", // "^.*wacc-examples/valid/IO/IOSequence.wacc.*$",
// "^.*wacc-examples/valid/pairs.*$", // "^.*wacc-examples/valid/pairs.*$",
"^.*wacc-examples/valid/runtimeErr.*$", //"^.*wacc-examples/valid/runtimeErr.*$",
// "^.*wacc-examples/valid/scope.*$", // "^.*wacc-examples/valid/scope.*$",
// "^.*wacc-examples/valid/sequence.*$", // "^.*wacc-examples/valid/sequence.*$",
// "^.*wacc-examples/valid/variables.*$", // "^.*wacc-examples/valid/variables.*$",