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
190 changes: 103 additions & 87 deletions src/main/scala/splatter/parsing.scala
Original file line number Diff line number Diff line change
@@ -1,93 +1,109 @@
package splatter
package parsing

case class P[+A](parse: String => Option[(A,String)]) {
import scala.annotation.tailrec

import P._

def flatMap[B](f: A => P[B]): P[B] =
P(s => parse(s) match {
case Some((a,r)) => f(a).parse(r)
case None => None
})

def map[B](f: A => B): P[B] =
P(s => parse(s) match {
case Some((a,r)) => Some(f(a),r)
case None => None
})

def |[A1 >: A](that: => P[A1]): P[A1] =
P(s => parse(s) match {
case None => that.parse(s)
case res@Some(_) => res
})

def ~[B](that: P[B]): P[B] =
for { _ <- this ; b <- that } yield b

private def loop[A1 >: A](s: String, acc: List[A1] = List.empty): (List[A1], String) =
parse(s) match {
case None => (acc.reverse, s)
case Some((a,ss)) => loop(ss, a :: acc)
}

def zeroOrMore: P[List[A]] =
P(s => Some(loop(s)))

def oneOrMore: P[List[A]] = {
P(s => parse(s).flatMap { case (a,ss) => Some(loop(ss, List(a)))})
}

def iff[B](p: A => Boolean)(t: P[B])(f: => P[B]): P[B] =
P(s => parse(s).flatMap { case (a,ss) => if (p(a)) t.parse(ss) else f.parse(ss)})
}

object P {

def run[A](p: P[A])(s: String): A =
p.parse(s) match {
case Some((a, "")) => a
case Some((_, rs)) => sys.error(s"unconsumed: $rs")
case None => sys.error(s"did not produce a value: ${s}")
}

def unit[A](a: A): P[A] =
P(s => Some(a, s))
object parsing:

def take: P[Char] =
P(s => if (s.nonEmpty) Some(s.head, s.tail) else None)

def fail[A]: P[A] =
P(_ => None)

def satisfy(p: Char => Boolean): P[Char] =
take.flatMap(c => if (p(c)) unit(c) else fail)

def end: P[Boolean] =
P(s => if (s.isEmpty) Some(true, "") else Some(false, s))

def digit: P[Char] =
satisfy(_.isDigit)
case class P[+A](parse: String => Option[(A,String)]):

def flatMap[B](f: A => P[B]): P[B] =
P(s =>
parse(s) match
case Some((a, r)) => f(a).parse(r)
case None => None
)

def map[B](f: A => B): P[B] =
P(s =>
parse(s) match
case Some((a, r)) => Some(f(a),r)
case None => None
)

def |[B >: A](that: => P[B]): P[B] =
P(s =>
parse(s) match
case None => that.parse(s)
case res@Some(_) => res
)

def ~[B](that: P[B]): P[B] =
for
_ <- this
b <- that
yield
b

@tailrec
private def loop[B >: A](s: String, acc: List[B] = List.empty): (List[B], String) =
parse(s) match
case None => (acc.reverse, s)
case Some((a, ss)) => loop(ss, a :: acc)

def digits: P[Long] =
satisfy(_.isDigit).oneOrMore.map(_.mkString("").toLong)
def zeroOrMore: P[List[A]] =
P(s => Some(loop(s)))

def oneOrMore: P[List[A]] =
P(s => parse(s).flatMap((a, ss) => Some(loop(ss, List(a)))))

def iff[B](p: A => Boolean)(t: P[B])(f: => P[B]): P[B] =
P(s => parse(s).flatMap((a, ss) => if (p(a)) t.parse(ss) else f.parse(ss)))

object P:

def run[A](p: P[A])(s: String): A =
p.parse(s) match
case Some((a, "")) => a
case Some((_, rs)) => sys.error(s"unconsumed: $rs")
case None => sys.error(s"did not produce a value: $s")

private def unit[A](a: A): P[A] =
P(s => Some(a, s))

def char(c: Char): P[Char] =
satisfy(_ == c)

def string(s: String): P[String] =
if (s.isEmpty) unit("") else for { _ <- char(s.head) ; _ <- string(s.tail) } yield s

def oneOf(s: String): P[Char] =
satisfy(s.contains(_))

def spaces: P[String] =
oneOf(" \t\n\r").zeroOrMore.map(_.mkString)

def token[A](p: P[A]): P[A] =
for { a <- p ; _ <- spaces } yield a

def keyword(word: String): P[String] =
token(string(word))
}
private def take: P[Char] =
P(s => if (s.nonEmpty) Some(s.head, s.tail) else None)

private def fail[A]: P[A] =
P(_ => None)

def satisfy(p: Char => Boolean): P[Char] =
take.flatMap(c => if (p(c)) unit(c) else fail)

def end: P[Boolean] =
P(s => if (s.isEmpty) Some(true, "") else Some(false, s))

def digit: P[Char] =
satisfy(_.isDigit)

def digits: P[Long] =
satisfy(_.isDigit).oneOrMore.map(_.mkString("").toLong)

private def char(c: Char): P[Char] =
satisfy(_ == c)

def string(s: String): P[String] =
if s.isEmpty then
unit("")
else
for
_ <- char(s.head)
_ <- string(s.tail)
yield
s

private def oneOf(s: String): P[Char] =
satisfy(s.contains(_))

def spaces: P[String] =
oneOf(" \t\n\r").zeroOrMore.map(_.mkString)

private def token[A](p: P[A]): P[A] =
for
a <- p
_ <- spaces
yield
a

def keyword(word: String): P[String] =
token(string(word))

48 changes: 29 additions & 19 deletions src/main/scala/splatter/stutter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ abstract class ExtractableOp[T](name: String) extends Extractable[T]:
val Op: Atom = Atom(name)

case class ExtractableOp1(name: String) extends ExtractableOp[Expr](name):
def extract = { case Lisp(Seq(Op, arg)) => arg }
def extract: PartialFunction[Expr, Expr] = {
case Lisp(Seq(Op, arg)) => arg
}

case class ExtractableOp2(name: String) extends ExtractableOp[(Expr,Expr)](name):
def extract = { case Lisp(Seq(Op, a, b)) => (a, b) }
def extract: PartialFunction[Expr, (Expr, Expr)] = {
case Lisp(Seq(Op, a, b)) => (a, b)
}

case class ExtractableOpN(name: String) extends ExtractableOp[Seq[Expr]](name):
def extract = { case Lisp(Op +: args) => args }
def extract: PartialFunction[Expr, Seq[Expr]] = {
case Lisp(Op +: args) => args
}

lazy val QuoteLit = ExtractableOp1("quote")
lazy val AtomLit = ExtractableOp1("atom")
Expand All @@ -55,11 +61,15 @@ val PrimitiveOps: Seq[Atom] =

// (lambda (p1 ... pn) e)
object Lambda extends ExtractableOp[(Seq[Expr],Expr)]("lambda"):
def extract = { case Lisp(Seq(Op, Lisp(parms), expr)) => (parms, expr) }
def extract: PartialFunction[Expr, (Seq[Expr], Expr)] = {
case Lisp(Seq(Op, Lisp(parms), expr)) => (parms, expr)
}

// (quote (lambda (p1 ... pn) e))
object QuotedLambda extends ExtractableOp[(Seq[Expr],Expr)]("quote"):
def extract = { case Lisp(Seq(Op, Lambda(parms, expr))) => (parms, expr) }
def extract: PartialFunction[Expr, (Seq[Expr], Expr)] = {
case Lisp(Seq(Op, Lambda(parms, expr))) => (parms, expr)
}

// ((lambda (p1 ... pn) e) a1 ... an)
object Function extends Extractable[(Seq[Expr],Expr,Seq[Expr])]:
Expand All @@ -72,9 +82,8 @@ object Function extends Extractable[(Seq[Expr],Expr,Seq[Expr])]:
object QuotedFunction extends Extractable[(Seq[Expr],Atom,Seq[Expr],Seq[Expr])]:
def extract: PartialFunction[Expr, (Seq[Expr], Atom, Seq[Expr], Seq[Expr])] = {
case Lisp(Lambda(parms, Lisp((op : Atom) +: fargs)) +: args)
if !PrimitiveOps.contains(op) &&
args.nonEmpty &&
QuotedLambda.is(args.head) => (parms, op, fargs, args)
if !PrimitiveOps.contains(op) && args.nonEmpty && QuotedLambda.is(args.head) =>
(parms, op, fargs, args)
}

def eval(e: Expr): Expr =
Expand All @@ -91,10 +100,9 @@ def eval(e: Expr): Expr =
// - eval all args except for quoted ones
// - replace the expression parms with the evaluated args
case Function(parms, expr, args) =>
val evaluated = args.map {
val evaluated = args.map:
case q @ QuoteLit(_) => q
case e : Expr => eval(e)
}
val replaced = replace(expr, parms.zip(evaluated).toMap)
eval(replaced)

Expand Down Expand Up @@ -135,11 +143,11 @@ def eval(s: String): Expr =
eval(Parser.parseLisp(s))

def replace(expr: Expr, parms: Map[Expr, Expr]): Expr =
Lisp(expr.subs.map({ // TODO clearly Lisp needs a `map`.
Lisp(expr.subs.map:
case a: Atom if parms.keySet.contains(a) => parms(a)
case a: Atom => a
case r: Lisp => replace(r, parms)
}))
)

object Parser:

Expand All @@ -149,22 +157,24 @@ object Parser:
def atom: P[Atom] =
satisfy(c => c.isLetter).oneOrMore.map(cs => Atom(cs.mkString("")))

def list: P[Lisp] =
for {
private def list: P[Lisp] =
for
_ <- keyword("(")
l <- expr.zeroOrMore.map(es => Lisp(es))
_ <- keyword(")")
} yield l
yield
l

def quote: P[Lisp] =
(keyword("'") | keyword("’")) ~ expr.map(e => Lisp(Seq(QuoteLit.Op, e)))

def expr: P[Expr] =
for {
private def expr: P[Expr] =
for
_ <- spaces
e <- (atom | list | quote)
e <- atom | list | quote
_ <- spaces
} yield e
yield
e

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