Skip to content

Commit

Permalink
Raise division-by-zero exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
melvic-ybanez committed Nov 9, 2023
1 parent 870884d commit 8c8e4b0
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 31 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ like this:
[Success] Ducks should quack!
[Success] Denji should say 'Woof!'
[Success] Class properties should be updated
Ran 169 tests. Successful: 169. Failed: 0.
Ran 172 tests. Successful: 172. Failed: 0.
```

The tests themselves are written in Dry (while the `testDry` command is written in Scala). You can see the directory
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/com/melvic/dry/interpreter/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ sealed trait Env {
env
}

@tailrec
private def ancestorAt(distance: Int): Env =
if (distance == 0) this
else
Expand All @@ -81,6 +82,7 @@ sealed trait Env {

object Env {
type Table = mutable.Map[String, Value]
type Register = Env => Env

/**
* An [[Env]] that has a pointer to its immediate enclosing environment, forming a Cactus Stack, to enable
Expand Down
15 changes: 7 additions & 8 deletions src/main/scala/com/melvic/dry/interpreter/Interpret.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package com.melvic.dry.interpreter

import com.melvic.dry.ast.Decl
import com.melvic.dry.interpreter.Env.LocalEnv
import com.melvic.dry.interpreter.Keys.{Errors, SuccessCount, TestCount}
import com.melvic.dry.interpreter.natives.Keys.{Errors, SuccessCount, TestCount}
import com.melvic.dry.interpreter.eval.Evaluate.Out
import com.melvic.dry.interpreter.eval.{Context, Evaluate}
import com.melvic.dry.interpreter.natives.{Assertions, Exceptions}
import com.melvic.dry.interpreter.values.Callable.Varargs
import com.melvic.dry.interpreter.values.Value.{Num, Str, ToValue, Types}
import com.melvic.dry.interpreter.values._
Expand All @@ -20,6 +21,7 @@ import com.melvic.dry.result.Result.implicits.ToResult
import java.nio.file.Path
import scala.collection.mutable.ListBuffer
import scala.io.StdIn.readLine
import scala.util.chaining.scalaUtilChainingOps

object Interpret {
def declarations(declarations: List[Decl], enclosing: Env, locals: Locals, sourcePaths: List[Path]): Out = {
Expand Down Expand Up @@ -60,9 +62,7 @@ object Interpret {

val natives: Env = Env.empty
.defineWith("print", Callable.unarySuccess(_)(arg => print(Value.show(arg)).unit))
// we don't have standard library functions yet, so we are building a dedicated function for println for now.
// Once, user-defined functions are supported, we can just replace this with a call to `print`, applied
// to a string that ends in a newline character
// TODO: Add this to stdlib or prelude, defined in terms of `print`
.defineWith("println", Callable.unarySuccess(_)(arg => println(Value.show(arg)).unit))
.defineWith(
"readLine",
Expand All @@ -76,15 +76,14 @@ object Interpret {
.defineWith("typeof", typeOf)
.define(TestCount, Num(0))
.define(SuccessCount, Num(0))
.defineWith("assert_true_with_message", Assertions.assertTrueWithMessage)
.defineWith("assert_error", Assertions.assertError)
.defineWith("show_test_results", Assertions.showTestResults)
.defineWith("Errors", errors)
.pipe(Assertions.register)
.pipe(Exceptions.register)

private def typeOf: Env => Callable = Callable.unarySuccess(_)(value => Str(Value.typeOf(value)))

private def errors(env: Env): DClass =
Errors.allErrors.foldLeft(DClass("Errors", Map.empty, env)) { (dClass, error) =>
Errors.allErrors.foldLeft(DClass.default("Errors", env)) { (dClass, error) =>
val fieldName = error.drop(2).dropRight(2).toUpperCase
dClass.addField(fieldName, Str(error))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private[eval] trait EvalDecl extends EvalStmt {
node match {
case ClassDecl(name, methods) =>
env.define(name, Value.None)
val klass = DClass(
val klass = new DClass(
name.lexeme,
methods
.map(method =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.melvic.dry.interpreter
package com.melvic.dry.interpreter.natives

import com.melvic.dry.Token
import com.melvic.dry.Token.TokenType
import com.melvic.dry.aux.Nel.{Many, One}
import com.melvic.dry.aux.Show.ShowInterpolator
import com.melvic.dry.interpreter.Keys.{SuccessCount, TestCount}
import Keys.{SuccessCount, TestCount}
import com.melvic.dry.interpreter.Env.Register
import com.melvic.dry.interpreter.values.Callable
import com.melvic.dry.interpreter.values.Value.{Num, Str, ToValue, Types, typeOf}
import com.melvic.dry.interpreter.{Env, Value}
import com.melvic.dry.result.Failure.RuntimeError
import com.melvic.dry.result.Failure.RuntimeError._
import com.melvic.dry.result.Result.implicits.ToResult
Expand Down Expand Up @@ -77,6 +79,11 @@ object Assertions {
Value.unit.ok
}

def register: Register =
_.defineWith("assert_true_with_message", Assertions.assertTrueWithMessage)
.defineWith("assert_error", Assertions.assertError)
.defineWith("show_test_results", Assertions.showTestResults)

private def addSuccess(env: Env, description: Value, successCount: Num): Unit = {
env.define(SuccessCount, Num(successCount.value + 1))
displaySuccess(description)
Expand Down
31 changes: 31 additions & 0 deletions src/main/scala/com/melvic/dry/interpreter/natives/Exceptions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.melvic.dry.interpreter.natives

import com.melvic.dry.interpreter.Env
import com.melvic.dry.interpreter.Env.Register
import com.melvic.dry.interpreter.values.DException._
import com.melvic.dry.interpreter.values.Value.{Types, typeOf}
import com.melvic.dry.interpreter.values.{Callable, DException, DInstance, Value}
import com.melvic.dry.result.Failure.RuntimeError
import com.melvic.dry.result.Result.Result

object Exceptions {
def register: Register =
_.defineWith("raise", raise)
.defineWith(DivisionByZero.name, DException(DivisionByZero, _))

private def raise(env: Env): Callable = Callable.withLineNo(1, env) { line =>
def invalidArgument(got: Value): Result[Value] =
RuntimeError.invalidArgument(Types.Exception, typeOf(got), line).fail

{
case (exception: DInstance) :: _ =>
def message: String =
DException.messageOf(exception).getOrElse("An exception occurred")

DException.Kind.of(exception).fold(invalidArgument(exception)) { case DivisionByZero.name =>
RuntimeError.divisionByZero(message, line).fail
}
case arg :: _ => invalidArgument(arg)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.melvic.dry.interpreter
package com.melvic.dry.interpreter.natives

object Keys {
val TestCount = "__tests_count__"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.melvic.dry.ast.Decl.Def
import com.melvic.dry.ast.Stmt.BlockStmt
import com.melvic.dry.ast.{Decl, Expr}
import com.melvic.dry.interpreter.Env
import com.melvic.dry.interpreter.Keys.LineNumber
import com.melvic.dry.interpreter.natives.Keys.LineNumber
import com.melvic.dry.interpreter.eval.{Context, Evaluate}
import com.melvic.dry.interpreter.values.Callable.Call
import com.melvic.dry.interpreter.values.Value.Returned
Expand Down
16 changes: 15 additions & 1 deletion src/main/scala/com/melvic/dry/interpreter/values/DClass.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package com.melvic.dry.interpreter.values

import com.melvic.dry.interpreter.Env
import com.melvic.dry.interpreter.values.Callable.{Function => DFunction}
import com.melvic.dry.interpreter.values.DClass.Methods
import com.melvic.dry.lexer.Lexemes
import com.melvic.dry.result.Result.implicits.ToResult

import scala.collection.mutable
import scala.util.chaining.scalaUtilChainingOps

final case class DClass(name: String, methods: Map[String, DFunction], enclosing: Env)
class DClass(val name: String, val methods: Methods, val enclosing: Env)
extends Callable
with Metaclass
with DObject {
Expand All @@ -24,3 +25,16 @@ final case class DClass(name: String, methods: Map[String, DFunction], enclosing

override val fields = mutable.Map.empty
}

object DClass {
type Methods = Map[String, DFunction]

def apply(name: String, methods: Methods, enclosing: Env): DClass =
new DClass(name, methods, enclosing)

def unapply(klass: DClass): Option[(String, Methods, Env)] =
Some(klass.name, klass.methods, klass.enclosing)

def default(name: String, env: Env): DClass =
new DClass(name, Map.empty, env)
}
47 changes: 47 additions & 0 deletions src/main/scala/com/melvic/dry/interpreter/values/DException.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.melvic.dry.interpreter.values

import com.melvic.dry.interpreter.Env
import com.melvic.dry.interpreter.values.DException.Kind
import com.melvic.dry.interpreter.values.Value.{Types, typeOf}
import com.melvic.dry.result.Failure.RuntimeError
import com.melvic.dry.result.Result.implicits.ToResult

class DException(val kind: Kind, val env: Env) extends DClass(kind.name, Map.empty, env) {
override def arity = 1

override def call = {
case args @ ((message: Value.Str) :: _) =>
super.call(args).flatMap { case instance: DInstance =>
instance.addField("exception_type", Value.Str(kind.name)).addField("message", message).ok
}
case arg :: _ => RuntimeError.invalidArgument(s"${Types.String}", typeOf(arg), lineNumber).fail
}
}

object DException {
sealed trait Kind {
val name: String = this.toString
}

object Kind {
def of(instance: DInstance): Option[String] =
asString(instance.fields.get("exception_type"))
}

case object DivisionByZero extends Kind

def apply(kind: Kind, env: Env): DException =
new DException(kind, env)

def unapply(exception: DException): Option[(Kind, Env)] =
Some(exception.kind, exception.env)

def messageOf(exception: DInstance): Option[String] =
asString(exception.fields.get("message"))

private def asString(value: Option[Value]): Option[String] =
value.flatMap {
case Value.Str(value) => Some(value)
case _ => None
}
}
2 changes: 2 additions & 0 deletions src/main/scala/com/melvic/dry/interpreter/values/Value.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ object Value {
val Dictionary = "dictionary"
val Callable = "callable"
val Tuple = "tuple"
val Exception = "exception"
}

def typeOf: Value => String = {
Expand All @@ -51,6 +52,7 @@ object Value {
case Num(_) => Types.Number
case Str(_) => Types.String
case Value.Unit => Types.Unit
case _: DException => Types.Exception
case _: DClass => Types.Class
case _: DInstance => Types.Instance
case _: DList => Types.List
Expand Down
40 changes: 24 additions & 16 deletions src/main/scala/com/melvic/dry/result/Failure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ object Failure {
sealed trait RuntimeError extends Failure

object RuntimeError {
final case class DivisionByZero(token: Token) extends RuntimeError
final case class DivisionByZero(message: String) extends RuntimeError
final case class InvalidOperand(token: Token, expected: List[String]) extends RuntimeError
final case class InvalidOperands(token: Token, expected: List[String]) extends RuntimeError
final case class UndefinedVariable(token: Token) extends RuntimeError
Expand All @@ -79,7 +79,10 @@ object Failure {
final case class ModuleNotFound(name: String, token: Token) extends RuntimeError

def divisionByZero(token: Token): RuntimeError =
DivisionByZero(token)
DivisionByZero(errorMsgWithToken(token, "Division by zero"))

def divisionByZero(message: String, line: Int): RuntimeError =
DivisionByZero(errorMsg(message, line))

def invalidOperand(operator: Token, expected: List[String]): RuntimeError =
InvalidOperand(operator, expected)
Expand Down Expand Up @@ -120,31 +123,36 @@ object Failure {
def moduleNotFound(name: String, token: Token): RuntimeError =
ModuleNotFound(name, token)

// TODO: Once all runtime errors get their own message fields, refactor this
def show: Show[RuntimeError] = {
case DivisionByZero(token) => errorMsg(token, "Division by zero")
case DivisionByZero(msg) => msg
case InvalidOperand(token, expected) =>
errorMsg(token, s"The operand must be any of the following: ${expected.toCsv}")
errorMsgWithToken(token, s"The operand must be any of the following: ${expected.toCsv}")
case InvalidOperands(token, expected) =>
errorMsg(token, s"All operands must be any of the following: ${expected.toCsv}")
case UndefinedVariable(token) => errorMsg(token, show"Undefined variable: $token")
case NotCallable(token) => errorMsg(token, "This expression is not callable.")
errorMsgWithToken(token, s"All operands must be any of the following: ${expected.toCsv}")
case UndefinedVariable(token) => errorMsgWithToken(token, show"Undefined variable: $token")
case NotCallable(token) => errorMsgWithToken(token, "This expression is not callable.")
case IncorrectArity(token, expected, got) =>
errorMsg(token, s"Incorrect arity. Expected: $expected. Got: $got")
errorMsgWithToken(token, s"Incorrect arity. Expected: $expected. Got: $got")
case DoesNotHaveProperties(obj, token) =>
errorMsg(token, show"$obj does not have properties or fields.")
case UndefinedProperty(token) => errorMsg(token, show"Undefined property: $token")
case UndefinedKey(key, token) => errorMsg(token, show"Undefined key: $key")
case CanNotApplyIndexOperator(obj, token) => errorMsg(token, show"Can not apply [] operator to $obj")
errorMsgWithToken(token, show"$obj does not have properties or fields.")
case UndefinedProperty(token) => errorMsgWithToken(token, show"Undefined property: $token")
case UndefinedKey(key, token) => errorMsgWithToken(token, show"Undefined key: $key")
case CanNotApplyIndexOperator(obj, token) =>
errorMsgWithToken(token, show"Can not apply [] operator to $obj")
case IndexOutOfBounds(index, line) =>
show"Runtime Error. Index out of bounds: $index\n[line $line]."
case InvalidIndex(index, token) => errorMsg(token, show"Invalid index: $index")
case InvalidIndex(index, token) => errorMsgWithToken(token, show"Invalid index: $index")
case InvalidArgument(expected, got, line) =>
show"Runtime Error. Invalid argument. Expected: $expected. Got: $got\n${showLine(line)}."
case ModuleNotFound(name, token) => errorMsg(token, show"Module not found: $name")
case ModuleNotFound(name, token) => errorMsgWithToken(token, show"Module not found: $name")
}

private def errorMsg(token: Token, message: String): String =
show"Runtime Error: $message\n${showLine(token.line)}. $token"
private def errorMsgWithToken(token: Token, message: String): String =
s"${errorMsg(message, token.line)} $token"

private def errorMsg(message: String, line: Int): String =
show"Runtime Error: $message\n$line."
}

sealed trait ResolverError extends Failure
Expand Down

0 comments on commit 8c8e4b0

Please sign in to comment.