Skip to content

Commit

Permalink
Support list syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
melvic-ybanez committed Nov 3, 2023
1 parent 1711304 commit da2cf21
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 60 deletions.
63 changes: 38 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<img width="875" alt="Sample Dry Program" src="https://github.com/melvic-ybanez/dry/assets/4519785/772dedf3-3ab6-4410-b82f-03ced48e5f44">

### 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.
Expand All @@ -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.
Expand All @@ -69,7 +74,7 @@ are true about you or your requirements, then you might want to give Dry a try:
$ java -jar dry-<version>.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:

```
Expand All @@ -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:
Expand Down Expand Up @@ -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:

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
<declaration> ::= <class> | <function> | <let> | <statement>
Expand Down Expand Up @@ -213,8 +224,10 @@ The syntax of Dry should be familiar to Python and Scala developers. Here's the
| ">>>" | "<=" <factor>)*
<factor> ::= <unary> ("/" | "*" | "%" <unary>)*
<unary> ::= ("!" | "-" | "+" | "not")* <call>
<primary> ::= <constant> | "self" | <identifier> | <tuple> | <dictionary> | "(" <expression> ")"
<primary> ::= <constant> | "self" | <identifier> | <list> | <tuple>
| <dictionary> | "(" <expression> ")"
<constant> ::= "false" | "true" | "none" | <number> | <string>
<list> ::= "[" (<expression> ("," <expression>*))? "]"
<tuple> ::= "(" (<expression> ("," | ("," <expression>)*))? ")"
<dictionary> ::= "{" (<key-value> ("," <key-value>)*)? "}"
<key-value> ::= <constant> ":" <expression>
Expand Down
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
9 changes: 6 additions & 3 deletions src/main/scala/com/melvic/dry/ast/Expr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 0 additions & 1 deletion src/main/scala/com/melvic/dry/interpreter/Interpret.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
20 changes: 14 additions & 6 deletions src/main/scala/com/melvic/dry/interpreter/eval/EvalExpr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 = {
Expand Down
26 changes: 20 additions & 6 deletions src/main/scala/com/melvic/dry/parsers/ExprParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -249,10 +250,23 @@ private[parsers] trait ExprParser { _: Parser =>
)
)

/**
* {{{<list> ::= "[" (<expression> ("," <expression>*))? "]"}}}
*/
def list: ParseResult[Expr.List] =
sequence[Expr](
TokenType.LeftBracket,
"[",
TokenType.RightBracket,
"]",
"at the start of lists",
"list elements"
)(_.expression.toOption).mapValue(Expr.List)

/**
* {{{<tuple> ::= "(" (<expression> ("," | ("," <expression>)*))? ")"}}}
*/
def tuple: ParseResult[Expr] =
def tuple: ParseResult[Expr.Tuple] =
for {
afterOpening <- consume(TokenType.LeftParen, "(", "at the start of tuple")
tupleElements <- afterOpening.expression.fold((_, _) =>
Expand All @@ -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
Expand All @@ -275,7 +289,7 @@ private[parsers] trait ExprParser { _: Parser =>
* <key-value> ::= <constant> ":" <expression>
* }}}
*/
def dictionary: ParseResult[Expr] =
def dictionary: ParseResult[Dictionary] =
sequence[(IndexKey, Expr)](
TokenType.LeftBrace,
"{",
Expand All @@ -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]] =
Expand Down
3 changes: 3 additions & 0 deletions src/main/scala/com/melvic/dry/parsers/ParseResult.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit da2cf21

Please sign in to comment.