From ceb5c70e81d166e8040e9b84a86d1892ce71f327 Mon Sep 17 00:00:00 2001 From: Melvic Ybanez Date: Sat, 4 Nov 2023 18:44:40 +0800 Subject: [PATCH] Expressions as dictionary keys --- README.md | 2 +- src/main/scala/com/melvic/dry/ast/Expr.scala | 19 +++++++------------ src/main/scala/com/melvic/dry/ast/Stmt.scala | 2 +- .../dry/interpreter/eval/EvalExpr.scala | 6 +++--- .../dry/interpreter/eval/Evaluate.scala | 4 ++-- .../com/melvic/dry/parsers/ExprParser.scala | 19 ++++--------------- .../scala/com/melvic/dry/result/Failure.scala | 14 +++++++------- tests/data_structs/test_dictionaries.dry | 8 +++++++- 8 files changed, 32 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index ff87162..dd647d7 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ like this: [Success] Ducks should quack! [Success] Denji should say 'Woof!' [Success] Class properties should be updated -Ran 167 tests. Successful: 167. Failed: 0. +Ran 168 tests. Successful: 168. Failed: 0. ``` The tests themselves are written in Dry (while the `testDry` command is written in Scala). You can see the directory diff --git a/src/main/scala/com/melvic/dry/ast/Expr.scala b/src/main/scala/com/melvic/dry/ast/Expr.scala index ea8c4ba..53281f4 100644 --- a/src/main/scala/com/melvic/dry/ast/Expr.scala +++ b/src/main/scala/com/melvic/dry/ast/Expr.scala @@ -10,8 +10,6 @@ import scala.{List => SList} sealed trait Expr object Expr { - type IndexKey = Either[Literal, Unary] - sealed trait Literal extends Expr object Literal { @@ -43,20 +41,20 @@ object Expr { final case class Get(obj: Expr, name: Token) extends Expr final case class Set(obj: Expr, name: Token, value: Expr) extends Expr - final case class IndexGet(obj: Expr, key: Either[Literal, Unary], token: Token) extends Expr - final case class IndexSet(obj: Expr, key: Either[Literal, Unary], value: Expr, token: Token) extends Expr + final case class IndexGet(obj: Expr, key: Expr, token: Token) extends Expr + final case class IndexSet(obj: Expr, key: Expr, value: Expr, token: Token) extends Expr final case class Self(keyword: Token) extends Expr final case class List(elems: SList[Expr]) extends Expr final case class Tuple(elems: SList[Expr]) extends Expr - final case class Dictionary(table: Map[Expr.IndexKey, Expr]) extends Expr + final case class Dictionary(table: Map[Expr, Expr]) extends Expr object Dictionary { def show: Show[Dictionary] = { case Dictionary(table) => - def fieldToString(field: (IndexKey, Expr)): String = - show"${Expr.showIndexKey(field._1)}: ${Expr.show(field._2)}" + def fieldToString(field: (Expr, Expr)): String = + show"${Expr.show(field._1)}: ${Expr.show(field._2)}" show"{ ${table.map(fieldToString).mkString(", ")} }" } @@ -77,14 +75,11 @@ object Expr { show"lambda(${params.map(Token.show).toCsv}) ${BlockStmt.fromDecls(body: _*)}" case Get(obj, name) => show"$obj.$name" case Set(obj, name, value) => show"$obj.$name = $value" - case IndexGet(obj, name, _) => show"$obj[${showIndexKey(name)}]" - case IndexSet(obj, name, value, _) => show"$obj[${showIndexKey(name)}] = $value" + case IndexGet(obj, name, _) => show"$obj[$name]" + case IndexSet(obj, name, value, _) => show"$obj[$name}] = $value" case Self(_) => "self" case List(elems) => show"[${Show.list(elems)}]" case Tuple(elems) => show"(${Show.list(elems)})" case dictionary: Dictionary => Dictionary.show(dictionary) } - - def showIndexKey(key: Expr.IndexKey): String = - key.fold(literal => show"$literal", unary => show"$unary") } diff --git a/src/main/scala/com/melvic/dry/ast/Stmt.scala b/src/main/scala/com/melvic/dry/ast/Stmt.scala index 447b274..2c83b50 100644 --- a/src/main/scala/com/melvic/dry/ast/Stmt.scala +++ b/src/main/scala/com/melvic/dry/ast/Stmt.scala @@ -56,7 +56,7 @@ object Stmt { } final case class ReturnStmt(keyword: Token, value: Expr) extends Stmt - final case class DeleteStmt(obj: Expr, key: Expr.IndexKey, token: Token) extends Stmt + final case class DeleteStmt(obj: Expr, key: Expr, token: Token) extends Stmt final case class Import(path: List[Token]) extends Stmt { def name: Token = path.last diff --git a/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala b/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala index 8c21707..f3f29bc 100644 --- a/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala +++ b/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala @@ -245,7 +245,7 @@ private[eval] trait EvalExpr { val dictFields = table.toList.foldFailFast(Result.succeed(Map.empty[Value, Value])) { case (result, (key, value)) => for { - evaluatedKey <- key.fold(Evaluate.literal(_), Evaluate.unary(_)) + evaluatedKey <- Evaluate.expr(key) evaluatedValue <- Evaluate.expr(value) } yield result + (evaluatedKey -> evaluatedValue) } @@ -258,12 +258,12 @@ private[eval] trait EvalExpr { Evaluate.expr(elem).map(_ :: result) } - private[eval] def index[A](obj: Expr, key: Expr.IndexKey, token: Token)( + private[eval] def index[A](obj: Expr, key: Expr, token: Token)( ifCanBeIndexed: PartialFunction[(Value, Value), Out] )(implicit context: Context[A]): Out = { for { evaluatedObj <- Evaluate.expr(obj) - evaluatedKey <- key.fold(Evaluate.literal(_), Evaluate.unary(_)) + evaluatedKey <- Evaluate.expr(key) orElse: PartialFunction[(Value, Value), Out] = { case (_: Value, _: Value) => RuntimeError.canNotApplyIndexOperator(obj, token).fail[Value] } diff --git a/src/main/scala/com/melvic/dry/interpreter/eval/Evaluate.scala b/src/main/scala/com/melvic/dry/interpreter/eval/Evaluate.scala index 0799ef7..0527e2f 100644 --- a/src/main/scala/com/melvic/dry/interpreter/eval/Evaluate.scala +++ b/src/main/scala/com/melvic/dry/interpreter/eval/Evaluate.scala @@ -1,7 +1,7 @@ package com.melvic.dry.interpreter.eval import com.melvic.dry.Token -import com.melvic.dry.ast.Expr.IndexKey +import com.melvic.dry.ast.Expr import com.melvic.dry.interpreter.values.Value import com.melvic.dry.result.Failure.RuntimeError import com.melvic.dry.result.Result.Result @@ -9,7 +9,7 @@ import com.melvic.dry.result.Result.Result object Evaluate extends EvalExpr with EvalDecl { type Out = Result[Value] - private[eval] def accessNumericIndex(evaluatedKey: Value, key: IndexKey, limit: Int, token: Token)( + private[eval] def accessNumericIndex(evaluatedKey: Value, key: Expr, limit: Int, token: Token)( access: Int => Out ): Out = evaluatedKey match { diff --git a/src/main/scala/com/melvic/dry/parsers/ExprParser.scala b/src/main/scala/com/melvic/dry/parsers/ExprParser.scala index 77b102c..5e615ee 100644 --- a/src/main/scala/com/melvic/dry/parsers/ExprParser.scala +++ b/src/main/scala/com/melvic/dry/parsers/ExprParser.scala @@ -171,19 +171,8 @@ private[parsers] trait ExprParser { _: Parser => } } - private[parsers] def indexAccess[A](keyF: IndexKey => A): ParseResult[A] = - indexKey(keyF).flatMapParser(_.consumeAfter(TokenType.RightBracket, "]", "index access")) - - private[parsers] def indexKey[A](keyF: IndexKey => A): ParseResult[A] = - expression - .flatMap { - case Step(literal: Literal, next) => ParseResult.succeed(keyF(Left(literal)), next) - case Step(unary @ Unary(Token(TokenType.Plus, _, _), Literal.Number(_)), next) => - ParseResult.succeed(keyF(Right(unary)), next) - case Step(unary @ Unary(Token(TokenType.Minus, _, _), Literal.Number(_)), next) => - ParseResult.succeed(keyF(Right(unary)), next) - case Step(_, next) => ParseResult.fail(ParseError.expected(peek, "key name", "after ["), next) - } + private[parsers] def indexAccess[A](keyF: Expr => A): ParseResult[A] = + expression.mapValue(keyF).flatMapParser(_.consumeAfter(TokenType.RightBracket, "]", "index access")) /** * Decides whether to construct a Call or a Lambda node. This is useful for partial application. @@ -290,7 +279,7 @@ private[parsers] trait ExprParser { _: Parser => * }}} */ def dictionary: ParseResult[Dictionary] = - sequence[(IndexKey, Expr)]( + sequence[(Expr, Expr)]( TokenType.LeftBrace, "{", TokenType.RightBrace, @@ -298,7 +287,7 @@ private[parsers] trait ExprParser { _: Parser => "at the start of dictionary", "dictionary elements" )( - _.indexKey(identity) + _.expression .flatMap { case Step(key, next) => next .consumeAfter(TokenType.Colon, ":", "dictionary key") diff --git a/src/main/scala/com/melvic/dry/result/Failure.scala b/src/main/scala/com/melvic/dry/result/Failure.scala index c28a521..d3c31b8 100644 --- a/src/main/scala/com/melvic/dry/result/Failure.scala +++ b/src/main/scala/com/melvic/dry/result/Failure.scala @@ -71,10 +71,10 @@ object Failure { final case class IncorrectArity(token: Token, expected: Int, got: Int) extends RuntimeError final case class DoesNotHaveProperties(obj: Expr, token: Token) extends RuntimeError final case class UndefinedProperty(token: Token) extends RuntimeError - final case class UndefinedKey(key: Expr.IndexKey, token: Token) extends RuntimeError + final case class UndefinedKey(key: Expr, token: Token) extends RuntimeError final case class CanNotApplyIndexOperator(obj: Expr, token: Token) extends RuntimeError final case class IndexOutOfBounds(index: Int, line: Int) extends RuntimeError - final case class InvalidIndex(index: Expr.IndexKey, token: Token) extends RuntimeError + final case class InvalidIndex(index: Expr, token: Token) extends RuntimeError final case class InvalidArgument(expected: String, got: String, line: Int) extends RuntimeError final case class ModuleNotFound(name: String, token: Token) extends RuntimeError @@ -102,7 +102,7 @@ object Failure { def undefinedProperty(token: Token): RuntimeError = UndefinedProperty(token) - def undefinedKey(key: Expr.IndexKey, token: Token): RuntimeError = + def undefinedKey(key: Expr, token: Token): RuntimeError = UndefinedKey(key, token) def canNotApplyIndexOperator(obj: Expr, token: Token): RuntimeError = @@ -111,7 +111,7 @@ object Failure { def indexOutOfBounds(index: Int, line: Int): RuntimeError = IndexOutOfBounds(index, line) - def invalidIndex(index: Expr.IndexKey, token: Token): RuntimeError = + def invalidIndex(index: Expr, token: Token): RuntimeError = InvalidIndex(index, token) def invalidArgument(expected: String, got: String, line: Int): RuntimeError = @@ -132,12 +132,12 @@ object Failure { errorMsg(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: ${Expr.showIndexKey(key)}") + 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") case IndexOutOfBounds(index, line) => show"Runtime Error. Index out of bounds: $index\n[line $line]." - case InvalidIndex(index, token) => errorMsg(token, show"Invalid index: ${Expr.showIndexKey(index)}") + case InvalidIndex(index, token) => errorMsg(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") diff --git a/tests/data_structs/test_dictionaries.dry b/tests/data_structs/test_dictionaries.dry index f6b1c61..99a32a3 100644 --- a/tests/data_structs/test_dictionaries.dry +++ b/tests/data_structs/test_dictionaries.dry @@ -89,6 +89,11 @@ class TestDictionary { // this will at least check if it works on a non-singleton, non-empty dictionary assert_equals("Size of retrieved fields", 2, {1: 2, 3: 4}.fields().size()); } + + def test_non_constant_keys() { + let dict = {1 + 1: 1}; + assert_equals("Dictionary with a non-constant key", 1, dict[2]); + } } let test_dict = TestDictionary(); @@ -99,4 +104,5 @@ test_dict.test_set(); test_dict.test_delete(); test_dict.test_nested(); test_dict.test_type(); -test_dict.test_fields_retrieval(); \ No newline at end of file +test_dict.test_fields_retrieval(); +test_dict.test_non_constant_keys(); \ No newline at end of file