Skip to content

Instantly share code, notes, and snippets.

@matfournier
Created July 30, 2020 05:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matfournier/3caddf54e347537de0fa7b14d2eee57e to your computer and use it in GitHub Desktop.
Save matfournier/3caddf54e347537de0fa7b14d2eee57e to your computer and use it in GitHub Desktop.
a stack language as an ADT with compile time checking
package stack
import cats.Monoid
import scala.util.Try
import cats.implicits._
import scala.reflect.macros.blackbox
import scala.language.experimental.macros
/** model our stack as an ADT */
sealed trait Stack extends Product with Serializable {self =>
import Stack._
def |>(op: Stack): Stack = Then(self, op)
}
object Stack {
final case class Push(v: Int) extends Stack
final case class Then(op1: Stack, op2: Stack) extends Stack
case object Pop extends Stack
case object Duplicate extends Stack
case object Plus extends Stack
case object Minus extends Stack
case object Empty extends Stack
implicit val stackMonoid: Monoid[Stack] = new Monoid[Stack] {
override def empty: Stack = Empty
override def combine(x: Stack, y: Stack): Stack = x |> y
}
/** convenience function create an arbitrary existing stack */
def of(values: List[Int]): Stack = values.foldMap(v => Push(v): Stack)
/** an interpreter for our ADT */
def eval(stack: Stack): Int = {
val init: List[Int] = Nil
/** Not stack safe */
def go(op: Stack, stack: List[Int]): List[Int] =
op match {
case Empty => stack
case Push(v) => v +: stack
case Pop => stack.tail
case Duplicate => stack.head +: stack
case Plus =>
val v: Int = scala.math.addExact(stack.head,stack.tail.head)
v +: stack.drop(2)
case Minus =>
val v = stack.head - stack.tail.head
if (v < 0) throw new ArithmeticException("negitive not allowed") else v +: stack
case Then(op1, op2) =>
go(op2, go(op1, stack))
}
// can't remember if they just wanted top, or entire stack
go(stack, init) match {
case Nil => throw new Exception("Stack cannot be empty")
case head::_ => head
}
}
/** interpret a string into a stack and evaluate it*/
def from(s: String): Int = {
val stack = s.split(" ").toList.map {
case v if v.forall(_.isDigit) => Push(v.toInt)
case "DUP" => Duplicate
case "POP" => Pop
case "+" => Plus
case "-" => Minus
case _ => throw new Exception("invalid symbol")
}
Stack.eval(stack.combineAll)
}
/**
* EVERYTHING BENEATH THIS IS FOR JOKES BUT WAS FUN LEARNING ABOUT MACROS
* boot up a repl:
* import stack.Stack
* import stack.Stack._
* then try stack"13 13 +" should return 26
* Compile time checking of stack"13 13 sjdhfs" fails with a compile error
* Compile time checking of stack"13 DUP 4 POP 5 DUP + DUP + -" passes
* compile time checking of stack"5 6 + -" fails with a compiler error
* if you had a stack"..." defined in another file, would be an inteliJ compiler error if an invalid
* stack language is evaluated at compile time.
*/
implicit class LiteralOps(val sc: StringContext) extends AnyVal {
def stack(args: Any*): Int = macro LiteralSyntaxMacros.stackInterpolator
}
object LiteralSyntaxMacros {
def stackInterpolator(c: blackbox.Context)(args: c.Expr[Any]*): c.Expr[Int] =
singlePartInterpolator(c)(
args,
"stack",
input => Try(Stack.from(input)).toOption.isDefined,
s => c.universe.reify(Stack.from(s.splice))
)
private def singlePartInterpolator[A](c: blackbox.Context)(
args: Seq[c.Expr[Any]],
typeName: String,
validate: String => Boolean,
construct: c.Expr[String] => c.Expr[A]): c.Expr[A] = {
import c.universe._
identity(args)
c.prefix.tree match {
case Apply(_, List(Apply(_, (lcp @ Literal(Constant(p: String))) :: Nil))) =>
val valid = validate(p)
if (valid) construct(c.Expr(lcp))
else c.abort(c.enclosingPosition, s"invalid $typeName")
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment