Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions src/main/scala/splatter/stutter/stutter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ object Expr:
val t = Atom("t")
val f = Lisp(Nil)

extension (e: Expr)
def eval: Expr =
evalExpr(e)

case class Atom(value: String) extends Expr:
def isAtom: Boolean = true
def isLisp: Boolean = false
Expand Down Expand Up @@ -84,62 +88,59 @@ object QuotedFunction extends Extractable[(Seq[Expr], Atom, Seq[Expr], Seq[Expr
(parms, op, fargs, args)
}

def eval(e: Expr): Expr =
def evalExpr(e: Expr): Expr =
e match

// quoted function calls first
// - unquote the lambda present in the first argument
case QuotedFunction(parms, op, fargs, args) =>
eval(Lisp(args.head match
Lisp(args.head match
case Lisp(Seq(QuoteLit.Op, lambda)) => lambda +: fargs
case _ => sys.error("lambda expression not quoted")))
case _ => sys.error("lambda expression not quoted")).eval

// function calls second
// - eval all args except for quoted ones
// - replace the expression parms with the evaluated args
case Function(parms, expr, args) =>
val evaluated = args.map:
case q @ QuoteLit(_) => q
case e : Expr => eval(e)
case e : Expr => e.eval
val replaced = replace(expr, parms.zip(evaluated).toMap)
eval(replaced)
replaced.eval

// primitive operations last
case QuoteLit(arg) => arg
case AtomLit(arg) => eval(arg) match
case AtomLit(arg) => arg.eval match
case a: Atom => Expr.t
case l: Lisp if l.isEmpty => Expr.t
case _ => Expr.f
case EqLit(a, b) => (eval(a), eval(b)) match
case EqLit(a, b) => (a.eval, b.eval) match
case (a1: Atom, a2: Atom) if a1 == a2 => Expr.t
case (l1: Lisp, l2: Lisp) if l1.isEmpty && l2.isEmpty => Expr.t
case _ => Expr.f
case CarLit(arg) => eval(arg) match
case CarLit(arg) => arg.eval match
case l: Lisp if l.isEmpty => sys.error("car on empty list")
case l: Lisp => l.subs.head
case a: Atom => sys.error(s"not a list: $a")
case CdrLit(arg) => eval(arg) match
case CdrLit(arg) => arg.eval match
case l: Lisp if l.subs.size <= 1 => sys.error("cdr on empty or singleton list")
case l: Lisp => Lisp(l.subs.tail)
case a: Atom => sys.error(s"not a list: $a")
case ConsLit(a, b) => (eval(a), eval(b)) match
case ConsLit(a, b) => (a.eval, b.eval) match
case (e, Lisp(es)) => Lisp(e +: es)
case (_, a: Atom) => sys.error(s"not a list: $a")
case CondLit(args) =>
args
.find:
case Lisp(Seq(p, e)) => eval(p) == Expr.t
case Lisp(Seq(p, e)) => p.eval == Expr.t
case e: Expr => sys.error(s"not a conditional $e")
.getOrElse(sys.error("undefined"))
match
case Lisp(Seq(_, expr)) => eval(expr)
case e => sys.error(s"has no argument list $e")
case Lisp(Seq(_, e)) => e.eval
case e => sys.error(s"has no argument list $e")
case _ =>
sys.error(s"invalid expression: $e")

def eval(s: String): Expr =
eval(Parser.parseLisp(s))

def replace(expr: Expr, parms: Map[Expr, Expr]): Expr =
Lisp(expr.subs.map:
case a: Atom if parms.keySet.contains(a) => parms(a)
Expand Down Expand Up @@ -176,7 +177,7 @@ object Parser:

def parseLisp(s: String): Expr =
run(expr)(s)

extension (s: String)
def parse: Expr =
parseLisp(s)
parseLisp(s)
39 changes: 21 additions & 18 deletions src/test/scala/splatter/stutter/ExpressionAxioms.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,44 @@ import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers._

class ExpressionAxioms extends AnyFunSpec:

import Parser.*
import Expr.*

describe("Expression axioms"):
it("quote expressions should yield the quoted expression"):
eval("(quote a)").toString should be ("a")
eval("'a").toString should be ("a")
eval("(quote (a b c))").toString should be ("(a b c)")
"(quote a)".parse.eval.toString should be ("a")
"'a".parse.eval.toString should be ("a")
"(quote (a b c))".parse.eval.toString should be ("(a b c)")

it("atom expressions should yield the atom `t` if the argument yields an atom or the empty list"):
eval("(atom 'a)").toString should be ("t")
eval("(atom '())").toString should be ("t")
eval("(atom (atom 'a))").toString should be ("t")
"(atom 'a)".parse.eval.toString should be ("t")
"(atom '())".parse.eval.toString should be ("t")
"(atom (atom 'a))".parse.eval.toString should be ("t")

it("atom expression should yield the empty list if the argument does not yield an atom or the empty list"):
eval("(atom '(atom a))").toString should be ("()")
eval("(atom '(a b c))").toString should be ("()")
"(atom '(atom a))".parse.eval.toString should be ("()")
"(atom '(a b c))".parse.eval.toString should be ("()")

it("eq expression should yield the atom `t` if both arguments yield the same atom or both the empty list"):
eval("(eq 'a 'a)").toString should be ("t")
eval("(eq '() '())").toString should be ("t")
"(eq 'a 'a)".parse.eval.toString should be ("t")
"(eq '() '())".parse.eval.toString should be ("t")

it("eq expression should yield the empty list if both arguments do not yield the same atom or the empty list"):
eval("(eq 'a 'b)").toString should be ("()")
"(eq 'a 'b)".parse.eval.toString should be ("()")

it("car expression should yield the first element of its argument list"):
eval("(car '(a b c))").toString should be ("a")
"(car '(a b c))".parse.eval.toString should be ("a")

it("cdr expression should yield everything after the first element of its argument list"):
eval("(cdr '(a b c))").toString should be ("(b c)")
"(cdr '(a b c))".parse.eval.toString should be ("(b c)")

it("cons expression should return a list containing the value of its first argument followed by the elements of the value of its second argument"):
eval("(cons 'a '(b c))").toString should be ("(a b c)")
eval("(cons 'a (cons 'b (cons 'c '())))").toString should be ("(a b c)")
eval("(car (cons 'a '(b c)))").toString should be ("a")
eval("(cdr (cons 'a '(b c)))").toString should be ("(b c)")
"(cons 'a '(b c))".parse.eval.toString should be ("(a b c)")
"(cons 'a (cons 'b (cons 'c '())))".parse.eval.toString should be ("(a b c)")
"(car (cons 'a '(b c)))".parse.eval.toString should be ("a")
"(cdr (cons 'a '(b c)))".parse.eval.toString should be ("(b c)")

it("cond expression (cond (p1 e1)...(pn en)) is evaluated as follows; the p expressions are evaluated in order until one returns t; when one is found, the value of the corresponding e expression is returned as the value of the whole cond expression."):
eval("(cond ((eq 'a 'b) 'first) ((atom 'a) 'second))").toString should be ("second")
"(cond ((eq 'a 'b) 'first) ((atom 'a) 'second))".parse.eval.toString should be ("second")

35 changes: 16 additions & 19 deletions src/test/scala/splatter/stutter/FunctionCallSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,25 @@ class FunctionCallSpec extends AnyFunSpec:

describe("A function call"):
it("works as specified in chapter 2 of roots of lisp..."):
eval(
"""
| ((lambda (x) (cons x '(b)))
| 'a)
|
""".stripMargin) should be ("(a b)".parse)
"""
| ((lambda (x) (cons x '(b)))
| 'a)
|
""".stripMargin.parse.eval should be ("(a b)".parse)

eval(
"""
| ((lambda (x y) (cons x (cdr y)))
| 'z
| '(a b c))
|
""".stripMargin) should be ("(z b c)".parse)
"""
| ((lambda (x y) (cons x (cdr y)))
| 'z
| '(a b c))
|
""".stripMargin.parse.eval should be ("(z b c)".parse)

it("treats parameters as operators in expressions as well as arguments"):
eval(
"""
| ((lambda (f) (f '(b c)))
| ’(lambda (x) (cons 'a x)))
|
""".stripMargin) should be ("(a b c)".parse)
"""
| ((lambda (f) (f '(b c)))
| ’(lambda (x) (cons 'a x)))
|
""".stripMargin.parse.eval should be ("(a b c)".parse)


describe("structural expression replacement"):
Expand Down