diff --git a/src/main/scala/minisql/Parser.scala b/src/main/scala/minisql/Parser.scala new file mode 100644 index 0000000..2fa5230 --- /dev/null +++ b/src/main/scala/minisql/Parser.scala @@ -0,0 +1,51 @@ +package minisql.parsing + +import minisql.ast +import minisql.ast.Ast +import scala.quoted.* + +type Parser[O <: Ast] = PartialFunction[Expr[?], Expr[O]] + +private[minisql] def parseFunction[X]( + x: Expr[X] +)(using Quotes): Expr[(List[Ast], Ast)] = { + import quotes.reflect.* + x.asTerm match { + case Lambda(vals, body) => + val paramExprs = vals.map { + case ValDef(n, _, _) => '{ ast.Ident(${ Expr(n) }) } + } + ??? + } +} +private def isNumeric(x: Expr[?])(using Quotes): Boolean = { + import quotes.reflect.* + x.asTerm.tpe match { + case t if t <:< TypeRepr.of[Int] => true + case t if t <:< TypeRepr.of[Long] => true + case t if t <:< TypeRepr.of[Float] => true + case t if t <:< TypeRepr.of[Double] => true + case t if t <:< TypeRepr.of[BigDecimal] => true + case t if t <:< TypeRepr.of[BigInt] => true + case t if t <:< TypeRepr.of[java.lang.Integer] => true + case t if t <:< TypeRepr.of[java.lang.Long] => true + case t if t <:< TypeRepr.of[java.lang.Float] => true + case t if t <:< TypeRepr.of[java.lang.Double] => true + case t if t <:< TypeRepr.of[java.math.BigDecimal] => true + case _ => false + } + +} + +def optionOperationParser( + astParser: Parser[Ast] +)(using Quotes): Parser[ast.OptionOperation] = { + case '{ ($x: Option[t]).isEmpty } => + '{ ast.OptionIsEmpty(${ astParser(x) }) } +} + +def binaryOperationParser( + astParser: Parser[Ast] +)(using Quotes): Parser[ast.BinaryOperation] = { + ??? +} diff --git a/src/main/scala/minisql/ast/Ast.scala b/src/main/scala/minisql/ast/Ast.scala index a1e79f2..35feb70 100644 --- a/src/main/scala/minisql/ast/Ast.scala +++ b/src/main/scala/minisql/ast/Ast.scala @@ -2,7 +2,7 @@ package minisql.ast import minisql.NamingStrategy -import scala.quoted._ +import scala.quoted.* sealed trait Ast { @@ -20,7 +20,6 @@ sealed trait Ast { override def apply(a: Ast) = super.apply(a.neutral) }.apply(this) - } //************************************************************ @@ -45,7 +44,8 @@ case class Entity( properties: List[PropertyAlias], renameable: Renameable ) extends Query { - override def neutral: Entity = Entity(name, properties, Renameable.neutral) + override inline def neutral: Entity = + Entity(name, properties, Renameable.neutral) } object Entity { @@ -182,7 +182,7 @@ case class ExternalIdent(name: String) extends Ast */ sealed trait Opinion[T] sealed trait OpinionValues[T <: Opinion[T]] { - def neutral: T + inline def neutral: T } sealed trait Visibility extends Opinion[Visibility] @@ -226,7 +226,7 @@ case class Property( visibility: Visibility ) extends Ast { - override def neutral: Property = + override inline def neutral: Property = copy(renameable = Renameable.neutral, visibility = Visibility.neutral) } @@ -374,13 +374,6 @@ sealed trait Lift extends Ast { val liftId: String } -object Lift { - private final val idGen = new java.util.concurrent.atomic.AtomicInteger() - private[minisql] def newLiftId(): Int = { - idGen.incrementAndGet() - } -} - sealed trait ScalarLift extends Lift case class ScalarValueLift( diff --git a/src/main/scala/minisql/ast/FromExprs.scala b/src/main/scala/minisql/ast/FromExprs.scala index 07a6cc6..8a1ef2a 100644 --- a/src/main/scala/minisql/ast/FromExprs.scala +++ b/src/main/scala/minisql/ast/FromExprs.scala @@ -1,6 +1,7 @@ package minisql.ast -import scala.quoted._ +import minisql.util.* +import scala.quoted.* private given FromExpr[PropertyAlias] with { def unapply(x: Expr[PropertyAlias])(using Quotes): Option[PropertyAlias] = @@ -282,7 +283,7 @@ given astFromExpr: FromExpr[Ast] = new FromExpr[Ast] { def unapply(e: Expr[Ast])(using Quotes): Option[Ast] = { val et = extractTerm(e.toTerm) et match { - case b: quotes.reflect.Block => fromBlock(b) + case b: quotes.reflect.Block => fromBlock(b).map(BetaReduction(_)) case b: quotes.reflect.Ident => Some(Ident(b.name)) case o => o.asExpr match { diff --git a/src/main/scala/minisql/dsl.scala b/src/main/scala/minisql/dsl.scala index aac07a0..94733ea 100644 --- a/src/main/scala/minisql/dsl.scala +++ b/src/main/scala/minisql/dsl.scala @@ -11,9 +11,10 @@ sealed trait Dsl { trait Query[E] extends Dsl -class EntityQuery[E](val ast: Ast) extends Query +case class EntityQuery[E](val ast: Ast) extends Query[E] extension [E](inline e: EntityQuery[E]) { + inline def mapAst[E1](inline f: Ast => Ast): EntityQuery[E1] = EntityQuery[E1](f(e.ast)) } @@ -49,9 +50,3 @@ private def compileImpl(x: Expr[Dsl])(using Quotes): Expr[Option[String]] = { } } - -case class Foo(id: Int) -inline def queryFooId = - query[Foo]("foo").mapAst[Int](x => - Map(x, Ident("x"), Property(Ident("x"), "id")) - ) diff --git a/src/main/scala/minisql/util/BetaReduction.scala b/src/main/scala/minisql/util/BetaReduction.scala new file mode 100644 index 0000000..0940564 --- /dev/null +++ b/src/main/scala/minisql/util/BetaReduction.scala @@ -0,0 +1,154 @@ +package minisql.util + +import minisql.ast.* +import scala.collection.immutable.{Map => IMap} + +case class BetaReduction(replacements: Replacements) + extends StatelessTransformer { + + override def apply(ast: Ast): Ast = + ast match { + + case ast if replacements.contains(ast) => + BetaReduction(replacements - ast - replacements(ast))(replacements(ast)) + + case Property(Tuple(values), name) => + apply(values(name.drop(1).toInt - 1)) + + case Property(CaseClass(tuples), name) => + apply(tuples.toMap.apply(name)) + + case FunctionApply(Function(params, body), values) => + val conflicts = values + .flatMap(CollectAst.byType[Ident]) + .map { i => + i -> Ident(s"tmp_${i.name}") + } + .toMap[Ast, Ast] + val newParams = params.map { p => + conflicts.getOrElse(p, p) + } + val bodyr = BetaReduction( + Replacements(conflicts ++ params.zip(newParams)) + ).apply(body) + apply( + BetaReduction(replacements ++ newParams.zip(values).toMap) + .apply(bodyr) + ) + + case Function(params, body) => + val newParams = params.map { p => + replacements.get(p) match { + case Some(i: Ident) => i + case _ => p + } + } + Function( + newParams, + BetaReduction(replacements ++ params.zip(newParams).toMap)(body) + ) + + case Block(statements) => + apply { + statements.reverse.tail + .foldLeft((IMap[Ast, Ast](), statements.last)) { + case ((map, stmt), line) => + BetaReduction(Replacements(map))(line) match { + case Val(name, body) => + val newMap = map + (name -> body) + val newStmt = BetaReduction(stmt, Replacements(newMap)) + (newMap, newStmt) + case _ => + (map, stmt) + } + } + ._2 + } + + case Foreach(query, alias, body) => + Foreach(query, alias, BetaReduction(replacements - alias)(body)) + case Returning(action, alias, prop) => + val t = BetaReduction(replacements - alias) + Returning(apply(action), alias, t(prop)) + + case ReturningGenerated(action, alias, prop) => + val t = BetaReduction(replacements - alias) + ReturningGenerated(apply(action), alias, t(prop)) + + case other => + super.apply(other) + } + + override def apply(o: OptionOperation): OptionOperation = + o match { + case other @ OptionTableFlatMap(a, b, c) => + OptionTableFlatMap(apply(a), b, BetaReduction(replacements - b)(c)) + case OptionTableMap(a, b, c) => + OptionTableMap(apply(a), b, BetaReduction(replacements - b)(c)) + case OptionTableExists(a, b, c) => + OptionTableExists(apply(a), b, BetaReduction(replacements - b)(c)) + case OptionTableForall(a, b, c) => + OptionTableForall(apply(a), b, BetaReduction(replacements - b)(c)) + case other @ OptionFlatMap(a, b, c) => + OptionFlatMap(apply(a), b, BetaReduction(replacements - b)(c)) + case OptionMap(a, b, c) => + OptionMap(apply(a), b, BetaReduction(replacements - b)(c)) + case OptionForall(a, b, c) => + OptionForall(apply(a), b, BetaReduction(replacements - b)(c)) + case OptionExists(a, b, c) => + OptionExists(apply(a), b, BetaReduction(replacements - b)(c)) + case other => + super.apply(other) + } + + override def apply(e: Assignment): Assignment = + e match { + case Assignment(alias, prop, value) => + val t = BetaReduction(replacements - alias) + Assignment(alias, t(prop), t(value)) + } + + override def apply(query: Query): Query = + query match { + case Filter(a, b, c) => + Filter(apply(a), b, BetaReduction(replacements - b)(c)) + case Map(a, b, c) => + Map(apply(a), b, BetaReduction(replacements - b)(c)) + case FlatMap(a, b, c) => + FlatMap(apply(a), b, BetaReduction(replacements - b)(c)) + case ConcatMap(a, b, c) => + ConcatMap(apply(a), b, BetaReduction(replacements - b)(c)) + case SortBy(a, b, c, d) => + SortBy(apply(a), b, BetaReduction(replacements - b)(c), d) + case GroupBy(a, b, c) => + GroupBy(apply(a), b, BetaReduction(replacements - b)(c)) + case Join(t, a, b, iA, iB, on) => + Join( + t, + apply(a), + apply(b), + iA, + iB, + BetaReduction(replacements - iA - iB)(on) + ) + case FlatJoin(t, a, iA, on) => + FlatJoin(t, apply(a), iA, BetaReduction(replacements - iA)(on)) + case DistinctOn(a, b, c) => + DistinctOn(apply(a), b, BetaReduction(replacements - b)(c)) + case _: Take | _: Entity | _: Drop | _: Union | _: UnionAll | + _: Aggregation | _: Distinct | _: Nested => + super.apply(query) + } +} + +object BetaReduction { + + def apply(ast: Ast, t: (Ast, Ast)*): Ast = + apply(ast, Replacements(t.toMap)) + + def apply(ast: Ast, replacements: Replacements): Ast = + BetaReduction(replacements).apply(ast) match { + case `ast` => ast + case other => apply(other, Replacements.empty) + } +} diff --git a/src/main/scala/minisql/util/Replacements.scala b/src/main/scala/minisql/util/Replacements.scala new file mode 100644 index 0000000..f0982e2 --- /dev/null +++ b/src/main/scala/minisql/util/Replacements.scala @@ -0,0 +1,52 @@ +package minisql.util + +import minisql.ast.Ast +import scala.collection.immutable.Map + +/** + * When doing beta reductions, the Opinions of AST elements need to be set to + * their neutral positions. + * + * For example, the `Property` AST element has a field called `renameable` which + * dicatates whether to use a `NamingStrategy` during tokenization in `SqlIdiom` + * (and other idioms) or not. Since this property only does things after + * Normalization, it should be completely transparent to beta reduction (all AST + * Opinion's have the same behavior). This is why we need to automatically set + * the `renameable` field to a pre-defined value every time `Property` is looked + * up. This is done via the `Ast.neutralize` method. + */ +case class Replacements(map: collection.Map[Ast, Ast]) { + + /** First transformed object to meet criteria * */ + def apply(key: Ast): Ast = + map.map { case (k, v) => (k.neutralize, v) } + .filter(_._1 == key.neutralize) + .head + ._2 + + /** First transformed object to meet criteria or none of none meets * */ + def get(key: Ast): Option[Ast] = + map.map { case (k, v) => (k.neutralize, v) } + .filter(_._1 == key.neutralize) + .headOption + .map(_._2) + + /** Does the map contain a normalized version of the view you want to see */ + def contains(key: Ast): Boolean = + map.map { case (k, v) => k.neutralize }.toList.contains(key.neutralize) + + def ++(otherMap: collection.Map[Ast, Ast]): Replacements = + Replacements(map ++ otherMap) + + def -(key: Ast): Replacements = { + val newMap = map.toList.filterNot { + case (k, v) => k.neutralize == key.neutralize + }.toMap + Replacements(newMap) + } +} + +object Replacements { + def empty: Replacements = + Replacements(Map()) +}