diff --git a/README.md b/README.md
index 93bbd0f..857098e 100644
--- a/README.md
+++ b/README.md
@@ -2,34 +2,37 @@
Dry is a dynamically-typed, high-level programming language currently being written in Scala.
-The image below shows an overview of Dry's syntax via examples. You can learn more about the
+The image below shows an overview of Dry's syntax via examples. You can learn more about the
language [here](#contents).
### Contents
+
1. [Introduction](#introduction)
- - [What is Dry](#what-is-dry)
- - [Why Use Dry](#why-use-dry)
+ - [What is Dry](#what-is-dry)
+ - [Why Use Dry](#why-use-dry)
1. [Installation](#installation)
1. [Getting Started](#getting-started)
- - [Starting the REPL](#starting-the-repl)
- - [Running a Dry Script](#running-a-dry-script)
+ - [Starting the REPL](#starting-the-repl)
+ - [Running a Dry Script](#running-a-dry-script)
1. [Running the tests](#running-the-tests)
1. Examples and Tests
- - [General](https://github.com/melvic-ybanez/dry/blob/main/examples/demo.dry)
- - [Classes](https://github.com/melvic-ybanez/dry/blob/main/tests/test_class.dry) and [Constructors](https://github.com/melvic-ybanez/dry/blob/main/tests/test_init.dry)
- - [Lists](https://github.com/melvic-ybanez/dry/blob/main/tests/test_lists.dry)
- - [Modules](https://github.com/melvic-ybanez/dry/blob/main/tests/test_imports.dry)
- - More [here](https://github.com/melvic-ybanez/dry/blob/main/tests/) and [here](https://github.com/melvic-ybanez/dry/blob/main/examples/)
+ - [General](https://github.com/melvic-ybanez/dry/blob/main/examples/demo.dry)
+ - [Classes](https://github.com/melvic-ybanez/dry/blob/main/tests/test_class.dry)
+ and [Constructors](https://github.com/melvic-ybanez/dry/blob/main/tests/test_init.dry)
+ - [Lists](https://github.com/melvic-ybanez/dry/blob/main/tests/test_lists.dry)
+ - [Modules](https://github.com/melvic-ybanez/dry/blob/main/tests/test_imports.dry)
+ - More [here](https://github.com/melvic-ybanez/dry/blob/main/tests/)
+ and [here](https://github.com/melvic-ybanez/dry/blob/main/examples/)
1. [Grammar](#grammar)
# Introduction
## What is Dry
-You can think of Dry as a Python-like programming language with curly braces and
-better support for functional programming (e.g. multi-line lambdas and partial
+You can think of Dry as a Python-like programming language with curly braces and
+better support for functional programming (e.g. multi-line lambdas and partial
function application). Dry is both _dynamically_ and _strongly_ typed, just like Python.
Dry was also heavily influenced by the Lox language, and you'll see why in the next section.
@@ -44,21 +47,23 @@ Dry started as a hobby project, when I was going through the first part of
language in that book, had also influenced the design of Dry.
However, as Dry started to grow, it became more and more of a language
-for cases where Python would normally shine.
-It became a language for people like myself back then (when I used Python at work),
+for cases where Python would normally shine.
+It became a language for people like myself back then (when I used Python at work),
who would sometimes wish Python had multi-line lambdas, supported braces and not overly rely on indentations.
Of course Dry doesn't have the libraries and tools that Python has, but if the following
are true about you or your requirements, then you might want to give Dry a try:
+
1. You want an expressive language to write scripts, and you don't need the help of a static type system.
1. You don't need much tools and libraries for your project.
1. You like Python but want to use first-class functions a lot.
# Installation
-1. The easiest way to install Dry on your system is to download a Dry executable (a Jar file) from the [releases](https://github.com/melvic-ybanez/dry/releases) page.
+1. The easiest way to install Dry on your system is to download a Dry executable (a Jar file) from
+ the [releases](https://github.com/melvic-ybanez/dry/releases) page.
I recommend you choose the one from the latest release. You can put the jar file in a directory of your choosing.
-1. Install [Java](https://www.java.com/en/), if you haven't already.
+1. Install [Java](https://www.java.com/en/), if you haven't already.
1. Since Dry is a Scala application, it is compiled to Java bytecode. This means you can
run the Jar file you downloaded in the previous section the same way you run any Java
Jar application, using the `java -jar` command.
@@ -69,7 +74,7 @@ are true about you or your requirements, then you might want to give Dry a try:
$ java -jar dry-.jar
```
- where `version` is the version of the release you downloaded. You should see a
+ where `version` is the version of the release you downloaded. You should see a
welcome message in the screen:
```
@@ -88,10 +93,10 @@ In Dry, like in some languages, you can either start a REPL, or run a script. Th
The welcome message you saw at the end of the [installation](#installation) section indicates that
you have entered the REPL (read-eval-print-loop) environment. That's what would
-happen if you ran Dry without specifying a path to a Dry script.
+happen if you ran Dry without specifying a path to a Dry script.
-Many languages such as Scala, Python, Haskell, or any of the known Lisp dialects like Clojure,
-have their own supports for REPLs, so programmers coming from a background of any of
+Many languages such as Scala, Python, Haskell, or any of the known Lisp dialects like Clojure,
+have their own supports for REPLs, so programmers coming from a background of any of
these languages might already be familiar with it.
The REPL allows you to enter expressions and declarations, and see their evaluated results immediately:
@@ -127,7 +132,7 @@ Bye!
## Running a Dry Script
To run a Dry script, you need to save the script to a file first and pass its path
-as an argument to the jar command. For instance, the code you saw at the beginning
+as an argument to the jar command. For instance, the code you saw at the beginning
of this ReadMe file is available in `examples/class_demo.dry` under this repository. We can
try running that with Dry:
@@ -142,13 +147,15 @@ quack!
As shown above, Dry source files end with the `.dry`.
# Running the tests
+
The project has a custom sbt command for running the test:
```
$ sbt testDry
```
-If everything works correctly, your console should print a bunch of assertion results. The end of the logs should look like this:
+If everything works correctly, your console should print a bunch of assertion results. The end of the logs should look
+like this:
```
[Success] Binding third param
@@ -170,7 +177,10 @@ If everything works correctly, your console should print a bunch of assertion re
[Success] Class properties should be updated
Ran 150 tests. Successful: 150. Failed: 0.
```
-The tests themselves are written in Dry (while the `testDry` command is written in Scala). You can see the directory containing them here: https://github.com/melvic-ybanez/dry/tree/main/tests. All the files in that directory that start with `test_` and have the Dry extension will be picked up by the `testDry` command.
+
+The tests themselves are written in Dry (while the `testDry` command is written in Scala). You can see the directory
+containing them here: https://github.com/melvic-ybanez/dry/tree/main/tests. All the files in that directory that start
+with `test_` and have the Dry extension will be picked up by the `testDry` command.
There is also a set of tests written in Scala, and you can run them like normal
Scala tests:
@@ -181,7 +191,8 @@ $ sbt test
# Grammar
-The syntax of Dry should be familiar to Python and Scala developers. Here's the grammar, written in [BNF](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form):
+The syntax of Dry should be familiar to Python and Scala developers. Here's the grammar, written
+in [BNF](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form):
```bnf
::= | | |
@@ -213,8 +224,10 @@ The syntax of Dry should be familiar to Python and Scala developers. Here's the
| ">>>" | "<=" )*
::= ("/" | "*" | "%" )*
::= ("!" | "-" | "+" | "not")*
- ::= | "self" | | | | "(" ")"
+ ::= | "self" | | |
+ | | "(" ")"
::= "false" | "true" | "none" | |
+ ::= "[" ( ("," *))? "]"
::= "(" ( ("," | ("," )*))? ")"
::= "{" ( ("," )*)? "}"
::= ":"
diff --git a/build.sbt b/build.sbt
index 92521f4..d5fced7 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,11 +1,11 @@
-ThisBuild / version := "0.3.0"
+ThisBuild / version := "0.4.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.13.8"
lazy val root = (project in file("."))
.settings(
name := "dry",
- assembly / assemblyJarName := "dry-0.3.0.jar",
+ assembly / assemblyJarName := "dry-0.4.0-SNAPSHOT.jar",
libraryDependencies ++= Seq(
"org.scalactic" %% "scalactic" % "3.2.17",
"org.scalatest" %% "scalatest" % "3.2.17" % "test",
diff --git a/src/main/scala/com/melvic/dry/ast/Expr.scala b/src/main/scala/com/melvic/dry/ast/Expr.scala
index 5612ca8..ea8c4ba 100644
--- a/src/main/scala/com/melvic/dry/ast/Expr.scala
+++ b/src/main/scala/com/melvic/dry/ast/Expr.scala
@@ -5,6 +5,7 @@ import com.melvic.dry.aux.Show
import com.melvic.dry.aux.Show.ShowInterpolator
import com.melvic.dry.aux.implicits.ListOps
import com.melvic.dry.{Show, Token}
+import scala.{List => SList}
sealed trait Expr
@@ -37,8 +38,8 @@ object Expr {
final case class Logical(left: Expr, operator: Token, right: Expr) extends Expr
- final case class Call(callee: Expr, arguments: List[Expr], paren: Token) extends Expr
- final case class Lambda(params: List[Token], body: List[Decl]) extends Expr
+ final case class Call(callee: Expr, arguments: SList[Expr], paren: Token) extends Expr
+ final case class Lambda(params: SList[Token], body: SList[Decl]) extends Expr
final case class Get(obj: Expr, name: Token) extends Expr
final case class Set(obj: Expr, name: Token, value: Expr) extends Expr
@@ -47,7 +48,8 @@ object Expr {
final case class Self(keyword: Token) extends Expr
- final case class Tuple(elems: List[Expr]) 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
@@ -78,6 +80,7 @@ object Expr {
case IndexGet(obj, name, _) => show"$obj[${showIndexKey(name)}]"
case IndexSet(obj, name, value, _) => show"$obj[${showIndexKey(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)
}
diff --git a/src/main/scala/com/melvic/dry/interpreter/Interpret.scala b/src/main/scala/com/melvic/dry/interpreter/Interpret.scala
index baba7f9..d2bf7b0 100644
--- a/src/main/scala/com/melvic/dry/interpreter/Interpret.scala
+++ b/src/main/scala/com/melvic/dry/interpreter/Interpret.scala
@@ -81,7 +81,6 @@ object Interpret {
.defineWith("assert_false", Assertions.assertFalse)
.defineWith("assert_error", Assertions.assertError)
.defineWith("show_test_results", Assertions.showTestResults)
- .defineWith("list", env => Varargs(env, elems => DList(elems.to(ListBuffer), env).ok))
.defineWith("Errors", errors)
private def typeOf: Env => Callable = Callable.unarySuccess(_)(value => Str(Value.typeOf(value)))
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 a8686f1..1310ecb 100644
--- a/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala
+++ b/src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala
@@ -3,7 +3,7 @@ package com.melvic.dry.interpreter.eval
import com.melvic.dry.Token
import com.melvic.dry.Token.TokenType
import com.melvic.dry.ast.Expr
-import com.melvic.dry.ast.Expr._
+import com.melvic.dry.ast.Expr.{List => _, _}
import com.melvic.dry.aux.implicits.ListOps
import com.melvic.dry.interpreter.Interpret
import com.melvic.dry.interpreter.Value.{Bool, Num, Str, None => VNone}
@@ -19,6 +19,7 @@ import com.melvic.dry.result.Result.implicits._
import scala.annotation.nowarn
import scala.collection.mutable
+import scala.collection.mutable.ListBuffer
//noinspection ScalaWeakerAccess
private[eval] trait EvalExpr {
@@ -37,6 +38,7 @@ private[eval] trait EvalExpr {
case indexGet: IndexGet => Evaluate.indexGet(indexGet)
case indexSet: IndexSet => Evaluate.indexSet(indexSet)
case self: Self => Evaluate.self(self)
+ case list: Expr.List => Evaluate.list(list)
case tuple: Tuple => Evaluate.tuple(tuple)
case dictionary: Dictionary => Evaluate.dictionary(dictionary)
}
@@ -231,12 +233,13 @@ private[eval] trait EvalExpr {
def self(implicit context: Context[Self]): Out = varLookup(node.keyword, node)
+ def list(implicit context: Context[Expr.List]): Out = node match {
+ case Expr.List(elems) =>
+ Evaluate.exprList(elems).map(elems => DList(elems.reverse.to(ListBuffer), env))
+ }
+
def tuple(implicit context: Context[Tuple]): Out = node match {
- case Tuple(elems) =>
- val evaluatedElems = elems.foldFailFast(Result.succeed(List.empty[Value])) { (result, elem) =>
- Evaluate.expr(elem).map(_ :: result)
- }
- evaluatedElems.map(elems => DTuple(elems.reverse, env))
+ case Tuple(elems) => exprList(elems).map(elems => DTuple(elems.reverse, env))
}
def dictionary(implicit context: Context[Dictionary]): Out = node match {
@@ -252,6 +255,11 @@ private[eval] trait EvalExpr {
dictFields.map(fields => DDictionary(fields.to(mutable.Map), env))
}
+ private[eval] def exprList(elems: List[Expr])(implicit context: Context[Expr]): Result[List[Value]] =
+ elems.foldFailFast(Result.succeed(List.empty[Value])) { (result, elem) =>
+ Evaluate.expr(elem).map(_ :: result)
+ }
+
private[eval] def index[A](obj: Expr, key: Expr.IndexKey, token: Token)(
ifCanBeIndexed: PartialFunction[(Value, Value), Out]
)(implicit context: Context[A]): Out = {
diff --git a/src/main/scala/com/melvic/dry/parsers/ExprParser.scala b/src/main/scala/com/melvic/dry/parsers/ExprParser.scala
index 1065c1e..77b102c 100644
--- a/src/main/scala/com/melvic/dry/parsers/ExprParser.scala
+++ b/src/main/scala/com/melvic/dry/parsers/ExprParser.scala
@@ -3,7 +3,7 @@ package com.melvic.dry.parsers
import com.melvic.dry.Token
import com.melvic.dry.Token.TokenType
import com.melvic.dry.ast.Expr
-import com.melvic.dry.ast.Expr.{Unary, _}
+import com.melvic.dry.ast.Expr.{List => _, _}
import com.melvic.dry.ast.Stmt.ReturnStmt
import com.melvic.dry.lexer.Lexemes
import com.melvic.dry.result.Failure.ParseError
@@ -233,7 +233,8 @@ private[parsers] trait ExprParser { _: Parser =>
.orElse(matchAny(TokenType.Identifier).map(p => Step(Variable(p.previousToken), p)))
.map(_.toParseResult)
.getOrElse(
- tuple
+ list
+ .orElse(tuple)
.orElse(dictionary)
.orElse(
matchAny(TokenType.LeftParen)
@@ -249,10 +250,23 @@ private[parsers] trait ExprParser { _: Parser =>
)
)
+ /**
+ * {{{ ::= "[" ( ("," *))? "]"}}}
+ */
+ def list: ParseResult[Expr.List] =
+ sequence[Expr](
+ TokenType.LeftBracket,
+ "[",
+ TokenType.RightBracket,
+ "]",
+ "at the start of lists",
+ "list elements"
+ )(_.expression.toOption).mapValue(Expr.List)
+
/**
* {{{ ::= "(" ( ("," | ("," )*))? ")"}}}
*/
- def tuple: ParseResult[Expr] =
+ def tuple: ParseResult[Expr.Tuple] =
for {
afterOpening <- consume(TokenType.LeftParen, "(", "at the start of tuple")
tupleElements <- afterOpening.expression.fold((_, _) =>
@@ -264,7 +278,7 @@ private[parsers] trait ExprParser { _: Parser =>
TokenType.RightParen,
")",
"tuple elements"
- )(_.expression.fold[Option[Step[Expr]]]((_, _) => None)(Some(_)))
+ )(_.expression.toOption)
} yield resetOfTuple.map(elems => Expr.Tuple(afterFirstExpr.value :: elems))
}
} yield tupleElements
@@ -275,7 +289,7 @@ private[parsers] trait ExprParser { _: Parser =>
* ::= ":"
* }}}
*/
- def dictionary: ParseResult[Expr] =
+ def dictionary: ParseResult[Dictionary] =
sequence[(IndexKey, Expr)](
TokenType.LeftBrace,
"{",
@@ -292,7 +306,7 @@ private[parsers] trait ExprParser { _: Parser =>
next.expression.map(_.map((key, _)))
}
}
- .fold[Option[Step[(IndexKey, Expr)]]]((_, _) => None)(Some(_))
+ .toOption
).mapValue(elements => Dictionary(elements.toMap))
private[parsers] def literal: Option[Step[Literal]] =
diff --git a/src/main/scala/com/melvic/dry/parsers/ParseResult.scala b/src/main/scala/com/melvic/dry/parsers/ParseResult.scala
index 677d8dc..7927ab2 100644
--- a/src/main/scala/com/melvic/dry/parsers/ParseResult.scala
+++ b/src/main/scala/com/melvic/dry/parsers/ParseResult.scala
@@ -50,6 +50,9 @@ final case class ParseResult[+A](result: Result[A], parser: Parser) {
def as[B](newValue: => B): ParseResult[B] =
mapValue(_ => newValue)
+
+ def toOption: Option[Step[A]] =
+ fold[Option[Step[A]]]((_, _) => None)(Some(_))
}
object ParseResult {
diff --git a/src/main/scala/com/melvic/dry/resolver/Resolve.scala b/src/main/scala/com/melvic/dry/resolver/Resolve.scala
index ce1eb49..600bd37 100644
--- a/src/main/scala/com/melvic/dry/resolver/Resolve.scala
+++ b/src/main/scala/com/melvic/dry/resolver/Resolve.scala
@@ -3,7 +3,7 @@ package com.melvic.dry.resolver
import com.melvic.dry.Token
import com.melvic.dry.ast.Decl.Let.{LetDecl, LetInit}
import com.melvic.dry.ast.Decl.{ClassDecl, Def, StmtDecl}
-import com.melvic.dry.ast.Expr._
+import com.melvic.dry.ast.Expr.{List => _, _}
import com.melvic.dry.ast.Stmt.IfStmt.{IfThen, IfThenElse}
import com.melvic.dry.ast.Stmt.Loop.While
import com.melvic.dry.ast.Stmt._
@@ -81,6 +81,7 @@ object Resolve {
case IndexGet(obj, _, _) => Resolve.expr(obj)
case IndexSet(obj, _, value, _) => Resolve.expr(value) >=> Resolve.expr(obj)
case self: Self => Resolve.self(self)
+ case list: Expr.List => Resolve.list(list)
case tuple: Tuple => Resolve.tuple(tuple)
case dict: Dictionary => Resolve.dictionary(dict)
}
@@ -152,20 +153,21 @@ object Resolve {
}
}
+ def list: Expr.List => Resolve = { case Expr.List(elems) =>
+ sequence(elems)(identity)
+ }
+
def tuple: Tuple => Resolve = { case Tuple(elems) =>
- context =>
- elems.foldFailFast(context.ok) { case (context, elem) =>
- Resolve.expr(elem)(context)
- }
+ sequence(elems)(identity)
}
def dictionary: Dictionary => Resolve = { case Dictionary(table) =>
- context =>
- table.toList.foldFailFast(context.ok) { case (context, (_, value)) =>
- Resolve.expr(value)(context)
- }
+ sequence(table.toList)(_._2)
}
+ private def sequence[A](elems: List[A])(f: A => Expr): Resolve = context =>
+ elems.foldFailFast(context.ok) { case (context, elem) => Resolve.expr(f(elem))(context) }
+
def local(name: Token): Expr => Resolve = { expr => context =>
val maybeFound = context.scopes.zipWithIndex.find { case (scope, _) =>
scope.contains(name.lexeme)
diff --git a/tests/data_structs/test_lists.dry b/tests/data_structs/test_lists.dry
index 7b8d85a..a4d9dc0 100644
--- a/tests/data_structs/test_lists.dry
+++ b/tests/data_structs/test_lists.dry
@@ -1,10 +1,11 @@
def test_list_str() {
- let xs = list(1, "two", 3);
+ let xs = [1, "two", 3];
assert_equals("Stringify lists", str(xs), "[1, two, 3]");
+ assert_equals("Empty list string representation", str([]), "[]");
}
def test_list_index() {
- let xs = list(1, "two", 3);
+ let xs = [1, "two", 3];
assert_equals("First element", xs._0, 1);
assert_equals("Second element", xs._1, "two");
@@ -15,12 +16,13 @@ def test_list_index() {
}
def test_list_size() {
- let xs = list(1, 2, 3, 4, 5);
- assert_equals("Size is the number of elements passed to the `list` function", xs.size(), 5);
+ 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 = list("first", "second", "third");
+ let xs = ["first", "second", "third"];
let success_count = 0;
if (xs._0 == xs.at(0)) success_count = success_count + 1;
@@ -32,20 +34,20 @@ def test_list_at_method() {
}
def test_list_add_method() {
- let xs = list("one");
+ 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 = list(1);
+ let xs = [1];
xs.at(2);
});
}
def test_list_type() {
- assert_equals("List type", typeof(list(1, 2)), "list");
+ assert_equals("List type", typeof([1, 2]), "list");
}
test_list_str();