Skip to content

Commit

Permalink
Catch blocks with raised exception objects
Browse files Browse the repository at this point in the history
  • Loading branch information
melvic-ybanez committed Nov 16, 2023
1 parent ec0ffe4 commit 469b57c
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 230 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ like this:
[Success] Ducks should quack!
[Success] Denji should say 'Woof!'
[Success] Class properties should be updated
Ran 172 tests. Successful: 172. Failed: 0.
Ran 178 tests. Successful: 178. Failed: 0.
```

The tests themselves are written in Dry (while the `testDry` command is written in Scala). You can see the directory
Expand Down
10 changes: 7 additions & 3 deletions src/main/scala/com/melvic/dry/ast/Stmt.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.melvic.dry.ast

import com.melvic.dry.ast.Decl.StmtDecl
import com.melvic.dry.ast.Expr.Variable
import com.melvic.dry.ast.Stmt.Loop.While
import com.melvic.dry.aux.{Nel, Show}
import com.melvic.dry.aux.Show.ShowInterpolator
Expand Down Expand Up @@ -67,13 +68,16 @@ object Stmt {
}
}

final case class CatchBlock(exception: Expr, block: BlockStmt, paren: Token)
final case class CatchBlock(instance: Option[Variable], kind: Variable, block: BlockStmt, paren: Token)

object CatchBlock {
implicit val implicitShow: Show[CatchBlock] = show

def show: Show[CatchBlock] = { case CatchBlock(exception, block, paren) =>
show"${Lexemes.Catch} $block"
def show: Show[CatchBlock] = {
case CatchBlock(None, kind, block, _) =>
show"${Lexemes.Catch} ($kind) $block"
case CatchBlock(Some(instance), kind, block, _) =>
show"${Lexemes.Catch} ($instance: $kind) $block"
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/main/scala/com/melvic/dry/interpreter/Repl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ trait Repl {
private def prioritizeErrors(errors: Nel[Failure]): List[Failure] = {
val allErrors = errors.toList.distinct
val priorities = allErrors.map {
case _: RuntimeError => 1
case _: ResolverError => 2
case _: ParseError => 3
case _: LexerError => 4
case _: Line => 5
case _: Failure.Raised => 1
case _: RuntimeError => 2
case _: ResolverError => 3
case _: ParseError => 4
case _: LexerError => 5
case _: Line => 6
}
val priorityTable = allErrors.zip(priorities).groupBy(_._2)
val topPrio = priorityTable.toList.minBy(_._1)
Expand Down
81 changes: 55 additions & 26 deletions src/main/scala/com/melvic/dry/interpreter/eval/EvalStmt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import com.melvic.dry.aux.Nel.{Many, One}
import com.melvic.dry.interpreter.Value.{Returned, Unit => VUnit}
import com.melvic.dry.interpreter.eval.Context.implicits._
import com.melvic.dry.interpreter.eval.Evaluate.Out
import com.melvic.dry.interpreter.values.DException.NoArgDException
import com.melvic.dry.interpreter.values.Value.{ToValue, Types}
import com.melvic.dry.interpreter.values.{DDictionary, DException, DList, DModule, Value}
import com.melvic.dry.interpreter.values._
import com.melvic.dry.interpreter.{Env, ModuleManager, Run}
import com.melvic.dry.result.Failure.RuntimeError
import com.melvic.dry.result.Result
import com.melvic.dry.result.Failure.{Raised, RuntimeError}
import com.melvic.dry.result.Result.Result
import com.melvic.dry.result.Result.implicits._
import com.melvic.dry.result.{Failure, Result}

//noinspection ScalaWeakerAccess
private[eval] trait EvalStmt {
Expand All @@ -35,8 +36,11 @@ private[eval] trait EvalStmt {
def exprStmt(implicit context: Context[ExprStmt]): Out =
Evaluate.expr(node.expr).map(_.unit)

def blockStmt(implicit context: Context[BlockStmt]): Out = {
val localEnv = Env.fromEnclosing(env)
def blockStmt(implicit context: Context[BlockStmt]): Out =
blockStmtWith(Env.fromEnclosing)

def blockStmtWith(envF: Env => Env)(implicit context: Context[BlockStmt]): Out = {
val localEnv = envF(env)
def recurse(outcome: Out, decls: List[Decl]): Out = {
decls match {
case Nil => outcome
Expand Down Expand Up @@ -94,32 +98,57 @@ private[eval] trait EvalStmt {

def tryCatch(implicit context: Context[TryCatch]): Out = node match {
case TryCatch(tryBlock, catchBlocks) =>
def handleException(raised: Raised) = {
def invalidArg(got: String, paren: Token): Failure =
RuntimeError.invalidArgument(Types.Exception, got, paren.line)

lazy val dExceptionKind: Option[String] = DException.kindOf(raised.instance)

def evalCatchBlock: CatchBlock => Result[Option[Value]] = {
case CatchBlock(None, exceptionKind, block, paren) =>
Evaluate.variable(exceptionKind).flatMap {
case DException(kind, _) if dExceptionKind.contains(kind.exceptionName) =>
Evaluate.blockStmt(block).map(Some(_))
case DException(_, _) => Right(None)
case arg => invalidArg(Value.typeOf(arg), paren).fail
}
case catchBlock @ CatchBlock(_, Variable(token @ Token(_, "GenericError", _)), _, paren) =>
dExceptionKind.toRight(One(invalidArg("unknown exception", paren))).flatMap { dExceptionKind =>
evalCatchBlock(catchBlock.copy(kind = Variable(token.copy(lexeme = dExceptionKind))))
}
case CatchBlock(Some(Variable(instance)), exceptionKind, block, paren) =>
Evaluate.variable(exceptionKind).flatMap {
case DException(kind, _) if dExceptionKind.contains(kind.exceptionName) =>
Evaluate
.blockStmtWith { env =>
val localEnv = Env.fromEnclosing(env)
localEnv.define(instance, raised.instance)
}(block)
.map(Some(_))
case DException(_, _) => Right(None)
case arg => invalidArg(Value.typeOf(arg), paren).fail
}
case CatchBlock(_, _, _, paren) => invalidArg("expression", paren).fail
}

def findCatchBlock(catchBlocks: Nel[CatchBlock]): Out =
catchBlocks match {
case One(head) => evalCatchBlock(head).flatMap(_.fold[Out](raised.fail)(_.ok))
case Many(head, tail) =>
evalCatchBlock(head).flatMap(_.fold[Out](findCatchBlock(tail))(_.ok))
}

findCatchBlock(catchBlocks)
}
Evaluate
.blockStmt(tryBlock)
.fold(
{
case One(runtimeError @ RuntimeError(kind, _, _)) =>
def invalidArg(got: String, paren: Token): Result[Option[Value]] =
RuntimeError.invalidArgument(Types.Exception, got, paren.line).fail

def evalCatchBlock: CatchBlock => Result[Option[Value]] = {
case CatchBlock(exception: Variable, block, paren) =>
Evaluate.variable(exception).flatMap {
case DException(`kind`, _) => Evaluate.blockStmt(block).map(Some(_))
case DException(_, _) => Right(None)
case arg => invalidArg(Value.typeOf(arg), paren)
}
case CatchBlock(_, _, paren) => invalidArg("expression", paren)
case One(failure @ Failure.Raised(_)) => handleException(failure)
case One(RuntimeError(kind, token, _)) =>
NoArgDException(kind, env).call(token)(Nil).flatMap { instance =>
handleException(Raised(instance.asInstanceOf[DInstance]))
}

def findCatchBlock(catchBlocks: Nel[CatchBlock]): Out =
catchBlocks match {
case One(head) => evalCatchBlock(head).flatMap(_.fold[Out](runtimeError.fail)(_.ok))
case Many(head, tail) =>
evalCatchBlock(head).flatMap(_.fold[Out](findCatchBlock(tail))(_.ok))
}

findCatchBlock(catchBlocks)
case errors => Result.failAll(errors)
},
_.ok
Expand Down
56 changes: 9 additions & 47 deletions src/main/scala/com/melvic/dry/interpreter/natives/Exceptions.scala
Original file line number Diff line number Diff line change
@@ -1,61 +1,23 @@
package com.melvic.dry.interpreter.natives

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

object Exceptions {
def register: Register =
_.defineWith("raise", raise)
.defineWith(DivisionByZero.exceptionName, DException(DivisionByZero, _))
.defineWith(UndefinedVariable.exceptionName, DException(UndefinedVariable, _))
.defineWith(InvalidOperand.exceptionName, DException(InvalidOperand, _))
.defineWith(InvalidOperands.exceptionName, DException(InvalidOperands, _))
.defineWith(NotCallable.exceptionName, DException(NotCallable, _))
.defineWith(IncorrectArity.exceptionName, DException(IncorrectArity, _))
.defineWith(DoesNotHaveProperties.exceptionName, DException(DoesNotHaveProperties, _))
.defineWith(UndefinedProperty.exceptionName, DException(UndefinedProperty, _))
.defineWith(UndefinedKey.exceptionName, DException(UndefinedKey, _))
.defineWith(CanNotApplyIndexOperator.exceptionName, DException(CanNotApplyIndexOperator, _))
.defineWith(IndexOutOfBounds.exceptionName, DException(IndexOutOfBounds, _))
.defineWith(InvalidIndex.exceptionName, DException(InvalidIndex, _))
.defineWith(InvalidArgument.exceptionName, DException(InvalidArgument, _))
.defineWith(ModuleNotFound.exceptionName, DException(ModuleNotFound, _))
def register: Register = { env =>
RuntimeError.Kind.all.foldLeft(env.defineWith("raise", raise)) { (env, kind) =>
env.defineWith(kind.exceptionName, DException(kind, _))
}
}

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")

def fail(error: (Token, String) => RuntimeError) =
error(Token.fromLine(line), message).fail

DException.kindOf(exception).fold(invalidArgument(exception)) {
case DivisionByZero.`exceptionName` => fail(RuntimeError.divisionByZero)
case UndefinedVariable.`exceptionName` => fail(RuntimeError.undefinedVariable)
case InvalidOperand.`exceptionName` => fail(RuntimeError.invalidOperand)
case InvalidOperands.`exceptionName` => fail(RuntimeError.invalidOperands)
case NotCallable.`exceptionName` => fail(RuntimeError.notCallable)
case IncorrectArity.`exceptionName` => fail(RuntimeError.incorrectArity)
case DoesNotHaveProperties.`exceptionName` => fail(RuntimeError.doesNotHaveProperties)
case UndefinedProperty.`exceptionName` => fail(RuntimeError.undefinedProperty)
case UndefinedKey.`exceptionName` => fail(RuntimeError.undefinedKey)
case CanNotApplyIndexOperator.`exceptionName` => fail(RuntimeError.canNotApplyIndexOperator)
case IndexOutOfBounds.`exceptionName` => RuntimeError.indexOutOfBounds(line, message).fail
case InvalidIndex.`exceptionName` => fail(RuntimeError.invalidIndex)
case InvalidArgument.`exceptionName` => RuntimeError.invalidArgument(line, message).fail
case ModuleNotFound.`exceptionName` => fail(RuntimeError.moduleNotFound)
}
case arg :: _ => invalidArgument(arg)
case (exception: DInstance) :: _ => Failure.Raised(exception).fail
case got :: _ => RuntimeError.invalidArgument(Types.Exception, typeOf(got), line).fail
}
}
}
7 changes: 1 addition & 6 deletions src/main/scala/com/melvic/dry/interpreter/natives/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package com.melvic.dry.interpreter.natives
import com.melvic.dry.result.Failure.RuntimeError

object Keys {
import RuntimeError.Kind._

val TestCount = "__tests_count__"
val SuccessCount = "__tests_success_count__"
val MainModule = "__main_module__"
Expand All @@ -17,9 +15,6 @@ object Keys {
}

def allErrors: List[String] =
(DivisionByZero :: InvalidOperand :: InvalidOperands :: UndefinedVariable ::
NotCallable :: IncorrectArity :: DoesNotHaveProperties :: UndefinedProperty ::
UndefinedKey :: CanNotApplyIndexOperator :: IndexOutOfBounds :: InvalidIndex ::
InvalidArgument :: ModuleNotFound :: Nil).map(fromErrorKind)
RuntimeError.Kind.all.map(fromErrorKind)
}
}
44 changes: 40 additions & 4 deletions src/main/scala/com/melvic/dry/interpreter/values/DException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.melvic.dry.interpreter.values

import com.melvic.dry.Token
import com.melvic.dry.interpreter.Env
import com.melvic.dry.interpreter.eval.Evaluate.Out
import com.melvic.dry.interpreter.values.DException.{Fields, addFields}
import com.melvic.dry.interpreter.values.Value.{Types, typeOf}
import com.melvic.dry.result.Failure.RuntimeError
import com.melvic.dry.result.Failure.RuntimeError.Kind
Expand All @@ -13,28 +15,62 @@ class DException(val kind: Kind, val env: Env) extends DClass(kind.exceptionName
override def call(token: Token) = {
case args @ ((message: Value.Str) :: _) =>
super.call(token)(args).flatMap { case instance: DInstance =>
instance.addField("exception_type", Value.Str(kind.exceptionName)).addField("message", message).ok
instance
.addField(Fields.Kind, Value.Str(kind.exceptionName))
.addField(Fields.Message, message)
.addField(Fields.Line, Value.Num(token.line))
.ok
}
case arg :: _ => RuntimeError.invalidArgument(s"${Types.String}", typeOf(arg), token.line).fail
}
}

object DException {
class NoArgDException(override val kind: Kind, override val env: Env) extends DException(kind, env) {
override def arity = 0

override def call(token: Token) = {
case Nil => super.call(token)(Value.Str(kind.exceptionName) :: Nil)
case _ => RuntimeError.invalidArgument(s"${Types.String}", typeOf(Value.Unit), token.line).fail
}
}

object NoArgDException {
def apply(kind: Kind, env: Env): NoArgDException =
new NoArgDException(kind, env)
}

object Fields {
val Message: String = "message"
val Kind: String = "kind"
val Line: String = "line"
}

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

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

def kindOf(instance: DInstance): Option[String] =
asString(instance.fields.get("exception_type"))
asString(instance.getField(Fields.Kind))

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

def lineOf(instance: DInstance): Option[Int] =
instance.getField(Fields.Line).flatMap(_.toNum).map(_.value.toInt)

private def asString(value: Option[Value]): Option[String] =
value.flatMap {
case Value.Str(value) => Some(value)
case _ => None
}

private def addFields(instance: DInstance, kind: Kind, message: String, token: Token): Out =
instance
.addField(Fields.Kind, Value.Str(kind.exceptionName))
.addField(Fields.Message, Value.Str(message))
.addField(Fields.Line, Value.Num(token.line))
.ok
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ trait DObject extends Value {

def get(name: Token): Result[Value] =
Result.fromOption(
fields.get(name.lexeme).orElse(klass.findMethod(name.lexeme).map(_.bind(this))),
getField(name.lexeme).orElse(klass.findMethod(name.lexeme).map(_.bind(this))),
RuntimeError.undefinedProperty(name)
)

def getField(name: String): Option[Value] =
fields.get(name)

def set(name: Token, value: Value): Value.None = {
fields += (name.lexeme -> value)
Value.None
Expand Down
24 changes: 19 additions & 5 deletions src/main/scala/com/melvic/dry/parsers/StmtParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private[parsers] trait StmtParser { _: Parser with DeclParser =>
/**
* {{{<block> ::= "{" <declaration>* "}"}}}
*/
def block(after: String): ParseResult[BlockStmt] =
def blockAfter(after: String): ParseResult[BlockStmt] =
for {
leftBrace <- consumeAfter(TokenType.LeftBrace, "{", after)
rest <- leftBrace.blockWithoutOpening
Expand Down Expand Up @@ -183,15 +183,29 @@ private[parsers] trait StmtParser { _: Parser with DeclParser =>
def catchStmt(parser: Parser): ParseResult[CatchBlock] =
for {
leftParen <- parser.consumeAfter(TokenType.LeftParen, "(", Lexemes.Catch)
exception <- leftParen
variable <- leftParen
.consumeAfter(TokenType.Identifier, "identifier", "(")
.map(p => Step(Variable(p.next.previousToken), p))
exception <- variable
.matchAny(TokenType.Colon)
.fold[ParseResult[(Option[Variable], Variable)]](
// the parsed variable becomes the exception type
ParseResult.fromStep(variable.map(v => (None, v)))
) {
// the parsed variable becomes the exception instance, and we parse
// another variable for the type
_.consumeAfter(TokenType.Identifier, "identifier", "(")
.map(p => Step((Some(variable.value), Variable(p.next.previousToken)), p))
}
rightParen <- exception.consumeAfter(TokenType.RightParen, ")", "identifier")
block <- rightParen.block(")")
} yield Step(CatchBlock(exception.value, block.value, leftParen.value), block.next)
block <- rightParen.blockAfter(")")
} yield Step(
CatchBlock(exception.value._1, exception.value._2, block.value, leftParen.value),
block.next
)

for {
tryBlock <- block(Lexemes.Try)
tryBlock <- blockAfter(Lexemes.Try)
catch_ <- tryBlock.consumeAfter(TokenType.Catch, Lexemes.Catch, "try block")
catchBlock <- catchStmt(catch_)
catchBlocks <- {
Expand Down
Loading

0 comments on commit 469b57c

Please sign in to comment.