diff --git a/src/main/scala/splatter/stutter/stutter.scala b/src/main/scala/splatter/stutter/stutter.scala index 15080b6..5acfbb6 100644 --- a/src/main/scala/splatter/stutter/stutter.scala +++ b/src/main/scala/splatter/stutter/stutter.scala @@ -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 @@ -84,15 +88,15 @@ 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 @@ -100,46 +104,43 @@ def eval(e: Expr): Expr = 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) @@ -176,7 +177,7 @@ object Parser: def parseLisp(s: String): Expr = run(expr)(s) - + extension (s: String) def parse: Expr = - parseLisp(s) + parseLisp(s) diff --git a/src/test/scala/splatter/stutter/ExpressionAxioms.scala b/src/test/scala/splatter/stutter/ExpressionAxioms.scala index 2b8ade1..e36c847 100644 --- a/src/test/scala/splatter/stutter/ExpressionAxioms.scala +++ b/src/test/scala/splatter/stutter/ExpressionAxioms.scala @@ -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") \ No newline at end of file diff --git a/src/test/scala/splatter/stutter/FunctionCallSpec.scala b/src/test/scala/splatter/stutter/FunctionCallSpec.scala index 199af24..6d6ff72 100644 --- a/src/test/scala/splatter/stutter/FunctionCallSpec.scala +++ b/src/test/scala/splatter/stutter/FunctionCallSpec.scala @@ -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"):