From 75ec614cda10e74d78db4f11e174b56bac446409 Mon Sep 17 00:00:00 2001 From: Melvic Ybanez Date: Fri, 3 Nov 2023 16:23:59 +0800 Subject: [PATCH] Bracket notation for list element access --- README.md | 2 +- .../dry/interpreter/eval/EvalExpr.scala | 6 +- .../dry/interpreter/values/Collections.scala | 15 --- .../dry/interpreter/values/Countable.scala | 13 +++ .../dry/interpreter/values/DDictionary.scala | 3 - .../melvic/dry/interpreter/values/DList.scala | 44 +------ .../dry/interpreter/values/DSequence.scala | 5 + .../dry/interpreter/values/DTuple.scala | 5 +- tests/data_structs/test_lists.dry | 108 +++++++++--------- 9 files changed, 80 insertions(+), 121 deletions(-) delete mode 100644 src/main/scala/com/melvic/dry/interpreter/values/Collections.scala create mode 100644 src/main/scala/com/melvic/dry/interpreter/values/Countable.scala create mode 100644 src/main/scala/com/melvic/dry/interpreter/values/DSequence.scala diff --git a/README.md b/README.md index 857098e..e1f1784 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ like this: [Success] Ducks should quack! [Success] Denji should say 'Woof!' [Success] Class properties should be updated -Ran 150 tests. Successful: 150. Failed: 0. +Ran 153 tests. Successful: 153. 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/interpreter/eval/EvalExpr.scala b/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala index 1310ecb..e3f2bb0 100644 --- a/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala +++ b/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala @@ -212,13 +212,13 @@ private[eval] trait EvalExpr { Evaluate.index(obj, key, token) { case (dict: DDictionary, evaluatedKey) => Result.fromOption(dict.getByKey(evaluatedKey), RuntimeError.undefinedKey(key, token)) - case (tuple: DTuple, evaluatedKey) => + case (sequence: DSequence, evaluatedKey) => evaluatedKey match { case Value.Num(index) if index % 1 == 0 => val intIndex = index.toInt - if (index < 0 || index >= tuple.elems.size) + if (index < 0 || index >= sequence.size) RuntimeError.indexOutOfBounds(intIndex, token.line).fail[Value] - else tuple.getByIndex(intIndex).ok + else sequence.getByIndex(intIndex).ok case _ => RuntimeError.invalidIndex(key, token).fail[Value] } } diff --git a/src/main/scala/com/melvic/dry/interpreter/values/Collections.scala b/src/main/scala/com/melvic/dry/interpreter/values/Collections.scala deleted file mode 100644 index 96dc392..0000000 --- a/src/main/scala/com/melvic/dry/interpreter/values/Collections.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.melvic.dry.interpreter.values - -import com.melvic.dry.interpreter.Env -import com.melvic.dry.result.Result.implicits.ToResult - -object Collections { - trait Countable { - def size: Int - - def addSizeMethod(env: Env): AddProperty = - _ + ("size" -> Callable.noArg(env) { - Value.Num(size).ok - }) - } -} diff --git a/src/main/scala/com/melvic/dry/interpreter/values/Countable.scala b/src/main/scala/com/melvic/dry/interpreter/values/Countable.scala new file mode 100644 index 0000000..317555a --- /dev/null +++ b/src/main/scala/com/melvic/dry/interpreter/values/Countable.scala @@ -0,0 +1,13 @@ +package com.melvic.dry.interpreter.values + +import com.melvic.dry.interpreter.Env +import com.melvic.dry.result.Result.implicits.ToResult + +trait Countable { + def size: Int + + def addSizeMethod(env: Env): AddProperty = + _ + ("size" -> Callable.noArg(env) { + Value.Num(size).ok + }) +} diff --git a/src/main/scala/com/melvic/dry/interpreter/values/DDictionary.scala b/src/main/scala/com/melvic/dry/interpreter/values/DDictionary.scala index d29b024..1db4efa 100644 --- a/src/main/scala/com/melvic/dry/interpreter/values/DDictionary.scala +++ b/src/main/scala/com/melvic/dry/interpreter/values/DDictionary.scala @@ -1,9 +1,6 @@ package com.melvic.dry.interpreter.values -import com.melvic.dry.Token -import com.melvic.dry.Token.TokenType import com.melvic.dry.interpreter.Env -import com.melvic.dry.interpreter.values.Collections.Countable import scala.collection.mutable diff --git a/src/main/scala/com/melvic/dry/interpreter/values/DList.scala b/src/main/scala/com/melvic/dry/interpreter/values/DList.scala index 0781dd3..57bdad0 100644 --- a/src/main/scala/com/melvic/dry/interpreter/values/DList.scala +++ b/src/main/scala/com/melvic/dry/interpreter/values/DList.scala @@ -1,56 +1,22 @@ package com.melvic.dry.interpreter.values -import com.melvic.dry.Token -import com.melvic.dry.Token.TokenType -import com.melvic.dry.ast.Expr import com.melvic.dry.interpreter.Env -import com.melvic.dry.result.Failure.RuntimeError -import com.melvic.dry.result.Result import scala.collection.mutable import scala.collection.mutable.ListBuffer -import scala.util.chaining.scalaUtilChainingOps -final case class DList(elems: ListBuffer[Value], env: Env) extends DObject with Collections.Countable { +final case class DList(elems: ListBuffer[Value], env: Env) extends DSequence { override def klass: Metaclass = DClass("List", Map.empty, env) override val fields: mutable.Map[String, Value] = - addIndexFields() - .pipe(addAtMethod) - .pipe(addSizeMethod(env)) - .pipe(addAddMethod) + addSizeMethod(env) + .andThen(addAddMethod)(Map.empty) .to(mutable.Map) - // TODO: Replace with bracket-notation - private def addIndexFields() = - elems.zipWithIndex - .map { case (elem, i) => ("_" + i) -> elem } - .to(Map) - - // TODO: Replace with bracket-notation - private def addAtMethod: AddProperty = - _ + ("at" -> Callable.withLineNo(1, env)(line => { case indexValue :: _ => - // This is temporary. We are going to replace this whole function - // with the [] operator anyway - val valueString = Value.show(indexValue) - - indexValue.toNum - .fold( - RuntimeError - .invalidIndex( - // again, these values are temporary and should be removed - Left(Expr.Literal.Str(valueString)), - Token(TokenType.Str(valueString), valueString, line) - ) - .fail[Value] - ) { num => - val index = num.value.toInt - Result.fromOption(elems.lift(index), RuntimeError.indexOutOfBounds(index, line)) - } - })) - private def addAddMethod: AddProperty = _ + ("add" -> Callable.unarySuccess(env)(elem => copy(elems = elems += elem))) override def size: Int = elems.size + + override def getByIndex(index: Int): Value = elems(index) } diff --git a/src/main/scala/com/melvic/dry/interpreter/values/DSequence.scala b/src/main/scala/com/melvic/dry/interpreter/values/DSequence.scala new file mode 100644 index 0000000..78e2cf4 --- /dev/null +++ b/src/main/scala/com/melvic/dry/interpreter/values/DSequence.scala @@ -0,0 +1,5 @@ +package com.melvic.dry.interpreter.values + +trait DSequence extends DObject with Countable { + def getByIndex(index: Int): Value +} diff --git a/src/main/scala/com/melvic/dry/interpreter/values/DTuple.scala b/src/main/scala/com/melvic/dry/interpreter/values/DTuple.scala index 0107f43..383d55b 100644 --- a/src/main/scala/com/melvic/dry/interpreter/values/DTuple.scala +++ b/src/main/scala/com/melvic/dry/interpreter/values/DTuple.scala @@ -1,10 +1,9 @@ package com.melvic.dry.interpreter.values import com.melvic.dry.interpreter.Env -import com.melvic.dry.interpreter.values.Collections.Countable import scala.collection.mutable -final case class DTuple(elems: List[Value], env: Env) extends DObject with Countable { +final case class DTuple(elems: List[Value], env: Env) extends DSequence { override def klass: Metaclass = DClass("Tuple", Map.empty, env) override def fields: mutable.Map[String, Value] = @@ -13,7 +12,7 @@ final case class DTuple(elems: List[Value], env: Env) extends DObject with Count override def size: Int = elems.size - def getByIndex(index: Int): Value = + override def getByIndex(index: Int): Value = elems(index) } diff --git a/tests/data_structs/test_lists.dry b/tests/data_structs/test_lists.dry index a4d9dc0..4c4f689 100644 --- a/tests/data_structs/test_lists.dry +++ b/tests/data_structs/test_lists.dry @@ -1,59 +1,53 @@ -def test_list_str() { - let xs = [1, "two", 3]; - assert_equals("Stringify lists", str(xs), "[1, two, 3]"); - assert_equals("Empty list string representation", str([]), "[]"); +class TestList { + def test_str() { + let xs = [1, "two", 3]; + assert_equals("Stringify lists", str(xs), "[1, two, 3]"); + assert_equals("Empty list string representation", str([]), "[]"); + } + + def test_get() { + let xs = [1, "two", 3]; + + assert_equals("First element", xs[0], 1); + assert_equals("Second element", xs[1], "two"); + assert_equals("Third element", xs[2], 3); + + let empty = []; + assert_error("Reading list elements with string index", Errors.INVALID_INDEX, lambda() { + empty[""]; + }); + assert_error("Reading list elements with index < 0", Errors.INDEX_OUT_OF_BOUNDS, lambda() { + empty[-1]; + }); + + assert_error("Reading list elements with index == size", Errors.INDEX_OUT_OF_BOUNDS, lambda() { + xs[3]; + }); + assert_error("Reading list elements with index > size", Errors.INDEX_OUT_OF_BOUNDS, lambda() { + xs[4]; + }); + } + + def test_size() { + let xs = [1, 2, 3, 4, 5]; + assert_equals("Non-empty list size", xs.size(), 5); + assert_equals("Empty list size", [].size(), 0); + } + + def test_add_method() { + let xs = ["one"]; + assert_equals("New element is added to the list", str(xs.add("two")), "[one, two]"); + assert_equals("Size should increase after adding new item", xs.size(), 2); + } + + def test_type() { + assert_equals("List type", typeof([1, 2]), "list"); + } } -def test_list_index() { - let xs = [1, "two", 3]; - - assert_equals("First element", xs._0, 1); - assert_equals("Second element", xs._1, "two"); - assert_equals("Third element", xs._2, 3); - - xs._0 = 10; - assert_equals("Updated first element", xs._0, 10); -} - -def test_list_size() { - let xs = [1, 2, 3, 4, 5]; - assert_equals("Non-empty list size", xs.size(), 5); - assert_equals("Empty list size", [].size(), 0); -} - -def test_list_at_method() { - let xs = ["first", "second", "third"]; - let success_count = 0; - - if (xs._0 == xs.at(0)) success_count = success_count + 1; - if (xs._1 == xs.at(1)) success_count = success_count + 1; - if (xs._2 == xs.at(2)) success_count = success_count + 1; - - // This description is a lie though - assert_equals("For all list `xs` and integer `i`, `xs.at(i)` is equal to `xs._`", success_count, 3); -} - -def test_list_add_method() { - let xs = ["one"]; - assert_equals("New element is added to the list", str(xs.add("two")), "[one, two]"); - assert_equals("Size should increase after adding new item", xs.size(), 2); -} - -def test_out_of_bounds() { - assert_error("Index out of bounds", Errors.INDEX_OUT_OF_BOUNDS, lambda() { - let xs = [1]; - xs.at(2); - }); -} - -def test_list_type() { - assert_equals("List type", typeof([1, 2]), "list"); -} - -test_list_str(); -test_list_index(); -test_list_size(); -test_list_at_method(); -test_out_of_bounds(); -test_list_type(); -test_list_add_method(); \ No newline at end of file +let test_list = TestList(); +test_list.test_str(); +test_list.test_get(); +test_list.test_size(); +test_list.test_type(); +test_list.test_add_method(); \ No newline at end of file