Skip to content

Possible simpler approach with very less macro (term match) using scala3 NamedTuple, opaque type and other features. #643

@jilen

Description

@jilen
//> using jvm system
//> using scala 3.7.1

import scala.quoted.*

enum BinOperator {
  case Eq
}

object BinOperator {
  given FromExpr[BinOperator] with {
    def unapply(x: Expr[BinOperator])(using Quotes): Option[BinOperator] =
      x match {
        case '{ BinOperator.Eq } => Some(BinOperator.Eq)
      }
  }
}

enum Ast {
  case Ident(name: String)
  case Prop(base: Ast, name: String)
  case Entity(table: String)
  case Map(query: Ast, ident: Ident, body: Ast)
  case Filter(query: Ast, ident: Ident, body: Ast)
  case Lift(name: String)
  case BinOp(op: BinOperator, left: Ast, right: Ast)
  case Constant(value: Long)
}

object Ast {

  given FromExpr[Constant] with {
    def unapply(x: Expr[Constant])(using Quotes) = x match {
      case '{ Ast.Constant(${ Expr(v) }) } => Some(Ast.Constant(v))
    }
  }

  given FromExpr[BinOp] with {
    def unapply(x: Expr[BinOp])(using Quotes) = x match {
      case '{ BinOp(${ Expr(op) }, ${ Expr(left) }, ${ Expr(right) }) } =>
        Some(BinOp(op, left, right))

    }
  }

  given FromExpr[Ast.Ident] with {
    def unapply(expr: Expr[Ast.Ident])(using Quotes): Option[Ast.Ident] =
      expr match {
        case '{ Ast.Ident(${ Expr(name) }) } => Some(Ast.Ident(name))
        case _                               => None
      }
  }

  given FromExpr[Ast.Prop] with {
    def unapply(expr: Expr[Ast.Prop])(using Quotes): Option[Ast.Prop] =
      expr match {
        case '{ Ast.Prop(${ Expr(base) }, ${ Expr(name) }) } =>
          Some(Ast.Prop(base, name))
        case _ => None
      }
  }

  given FromExpr[Ast.Entity] with {
    def unapply(expr: Expr[Ast.Entity])(using Quotes): Option[Ast.Entity] =
      expr match {
        case '{ Ast.Entity(${ Expr(table) }) } => Some(Ast.Entity(table))
        case _                                 => None
      }
  }

  given FromExpr[Ast.Map] with {
    def unapply(expr: Expr[Ast.Map])(using Quotes): Option[Ast.Map] =
      expr match {
        // query, ident, and body are Ast types, so we extract their Exprs and unapply explicitly
        case '{ Ast.Map(${ Expr(q) }, ${ Expr(id) }, ${ Expr(body) }) } =>
          Some(Ast.Map(q, id, body))
        case _ => None
      }
  }

  given FromExpr[Ast.Filter] with {
    def unapply(expr: Expr[Ast.Filter])(using Quotes): Option[Ast.Filter] =
      expr match {
        // query, ident, and body are Ast types, so we extract their Exprs and unapply explicitly
        case '{ Ast.Filter(${ Expr(q) }, ${ Expr(id) }, ${ Expr(body) }) } =>
          Some(Ast.Filter(q, id, body))
        case _ => None
      }
  }

  // FromExpr for Ast.Lift
  given FromExpr[Ast.Lift] with {
    def unapply(expr: Expr[Ast.Lift])(using Quotes): Option[Ast.Lift] =
      expr match {
        case '{ Ast.Lift(${ Expr(name) }) } => Some(Ast.Lift(name))
        case _                              => None
      }
  }

  // Composite FromExpr for the Ast enum
  given FromExpr[Ast] with {
    def unapply(expr: Expr[Ast])(using Quotes): Option[Ast] = {
      import quotes.reflect.*
      extractTerm(expr.asTerm).asExpr match {
        case '{
              val p: EntityRef[t] = $pb
              $e: Ast
            } =>
          e.value
        case '{ $e: Ast.Ident }    => e.value
        case '{ $e: Ast.Prop }     => e.value
        case '{ $e: Ast.Entity }   => e.value
        case '{ $e: Ast.Map }      => e.value
        case '{ $e: Ast.Filter }   => e.value
        case '{ $e: Ast.Lift }     => e.value
        case '{ $e: Ast.BinOp }    => e.value
        case '{ $e: Ast.Constant } => e.value
        case e =>
          println(e.show)
          None
      }
    }
  }
}

private def extractTerm(using Quotes)(x: quotes.reflect.Term) = {
  import quotes.reflect.*
  def unwrapTerm(t: Term): Term = t match {
    case Inlined(_, _, o) => unwrapTerm(o)
    case Block(Nil, last) => last
    case Typed(t, _) =>
      unwrapTerm(t)
    case Select(t, "$asInstanceOf$") =>
      unwrapTerm(t)
    case TypeApply(t, _) =>
      unwrapTerm(t)
    case o => o
  }
  val o = unwrapTerm(x)
  o
}

opaque type Quoted <: Ast = Ast

opaque type QuotedExpr[V] <: Quoted = Ast

object Quoted {

  inline def apply(inline x: Ast): Ast = x

  inline def const(inline v: Long): QuotedExpr[Long] = Ast.Constant(v)
}

opaque type Col[E] <: Quoted = Ast

object Col {
  extension [E](inline e: Col[E]) {
    inline def ===(inline e1: QuotedExpr[E]): QuotedExpr[Boolean] =
      Ast.BinOp(BinOperator.Eq, e, e1)
  }
}

opaque type Query[A] <: Quoted & Ast = Ast

object Query {

  inline def apply[A](inline x: Ast): Query[A] = x

  extension [A](inline qa: Query[A]) {
    inline def map[B <: Quoted](inline f: EntityRef[A] => B): Query[B] =
      Ast.Map(qa, Ast.Ident("x"), f(EntityRef[A]))

    inline def filter(inline f: EntityRef[A] => QuotedExpr[Boolean]) = {
      Ast.Filter(qa, Ast.Ident("x"), f(EntityRef[A]))
    }
  }
}

class EntityRef[A] extends Selectable {
  type Fields = A
}

extension [A](e: EntityRef[A]) {
  inline def selectDynamic(inline n: String) = Ast.Prop(Ast.Ident("x"), n)
}

inline def staticAst(inline x: Ast): Option[String] = ${
  staticAstImpl('x)
}
private def staticAstImpl(x: Expr[Ast])(using Quotes): Expr[Option[String]] = {
  x.value match {
    case Some(astInstance) =>
      Expr(Some(astInstance.toString))
    case None =>
      Expr(None)
  }
}
 inline def Persons: Query[(id: Col[Long])] = Query(Ast.Entity("person"))

staticAst(Persons.map(p => p.id))
// Some(Map(Entity(person),Ident(x),Prop(Ident(x),id)))

staticAst(Persons.filter(p => p.id === Quoted.const(1L)))
// Some(Filter(Entity(person),Ident(x),BinOp(Eq,Prop(Ident(x),id),Constant(1))))

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions