diff --git a/core/src/main/scala/squid/lang/InspectableBase.scala b/core/src/main/scala/squid/lang/InspectableBase.scala index df6cee4c..29cfa83e 100644 --- a/core/src/main/scala/squid/lang/InspectableBase.scala +++ b/core/src/main/scala/squid/lang/InspectableBase.scala @@ -95,7 +95,7 @@ trait InspectableBase extends IntermediateBase with quasi.QuasiBase with TraceDe } protected implicit class ProtectedInspectableRepOps(private val self: Rep) { - def extract (that: Rep) = baseSelf.extract(self, that) + def extract (that: Rep) = baseSelf.extractRep(self, that) } implicit class InspectableRepOps(private val self: Rep) { /** Note: this is only a to-level call to `base.extractRep`; not supposed to be called in implementations of `extract` itself */ diff --git a/core/src/main/scala/squid/lang/IntermediateBase.scala b/core/src/main/scala/squid/lang/IntermediateBase.scala index 14439c66..8212e429 100644 --- a/core/src/main/scala/squid/lang/IntermediateBase.scala +++ b/core/src/main/scala/squid/lang/IntermediateBase.scala @@ -21,6 +21,7 @@ trait IntermediateBase extends Base { ibase: IntermediateBase => def reinterpret(r: Rep, newBase: Base)(extrudedHandle: (BoundVal => newBase.Rep) = DefaultExtrudedHandler): newBase.Rep + // TODO rename to `defaultValue` def nullValue[T: IRType]: IR[T,{}] diff --git a/core/src/main/scala/squid/lang/Optimizer.scala b/core/src/main/scala/squid/lang/Optimizer.scala index f9ded26f..913e7ba7 100644 --- a/core/src/main/scala/squid/lang/Optimizer.scala +++ b/core/src/main/scala/squid/lang/Optimizer.scala @@ -7,7 +7,7 @@ trait Optimizer { //final def optimizeRep(pgrm: Rep): Rep = pipeline(pgrm) final def optimizeRep(pgrm: Rep): Rep = { // TODO do this Transformer's `pipeline`......? val r = pipeline(pgrm) - if (!(r eq pgrm)) substitute(r) else r // only calls substitute if a transformation actually happened + if (!(r eq pgrm)) insertAfterTransformation(r) else r // only calls insertAfterTransformation if a transformation actually happened } final def optimize[T,C](pgrm: IR[T,C]): IR[T,C] = `internal IR`[T,C](optimizeRep(pgrm.rep)) diff --git a/core/src/main/scala/squid/quasi/MetaBases.scala b/core/src/main/scala/squid/quasi/MetaBases.scala index f423f4c4..cc7f4b03 100644 --- a/core/src/main/scala/squid/quasi/MetaBases.scala +++ b/core/src/main/scala/squid/quasi/MetaBases.scala @@ -170,6 +170,8 @@ trait MetaBases { def hole(name: String, typ: TypeRep): Rep = q"$Base.hole($name, $typ)" def hopHole(name: String, typ: TypeRep, yes: List[List[BoundVal]], no: List[BoundVal]): Rep = q"$Base.hopHole($name, $typ, ${yes map (_ map (_._2))}, ${no map (_._2)})" + override def hopHole2(name: String, typ: TypeRep, argss: List[List[Rep]], visible: List[BoundVal]): Rep = + q"$Base.hopHole2($name, $typ, ${argss}, ${visible map (_._2)})" def splicedHole(name: String, typ: TypeRep): Rep = q"$Base.splicedHole($name, $typ)" def substitute(r: => Rep, defs: Map[String, Rep]): Rep = diff --git a/core/src/main/scala/squid/quasi/QuasiBase.scala b/core/src/main/scala/squid/quasi/QuasiBase.scala index 92120b53..ffac516d 100644 --- a/core/src/main/scala/squid/quasi/QuasiBase.scala +++ b/core/src/main/scala/squid/quasi/QuasiBase.scala @@ -33,6 +33,8 @@ self: Base => def substitute(r: => Rep, defs: Map[String, Rep]): Rep def substituteLazy(r: => Rep, defs: Map[String, () => Rep]): Rep = substitute(r, defs map (kv => kv._1 -> kv._2())) + def insertAfterTransformation(r: => Rep, defs: Map[String, Rep]): Rep = substitute(r, defs) + /** Not yet used: should eventually be defined by all IRs as something different than hole */ def freeVar(name: String, typ: TypeRep) = hole(name, typ) @@ -46,6 +48,14 @@ self: Base => * this should check that the extracted term does not contain any reference to a bound value contained in the `no` * parameter, and it should extract a function term with the arity of the `yes` parameter. */ def hopHole(name: String, typ: TypeRep, yes: List[List[BoundVal]], no: List[BoundVal]): Rep + def hopHole2(name: String, typ: TypeRep, args: List[List[Rep]], visible: List[BoundVal]): Rep = { + // TODO remove `hopHole` and implement this correctly everywhere + //val vars = args map (_ map (_.asInstanceOf[BoundVal]))\ + // ^ Note: this will succeed even if there are some non-BoundVal, because BoundVal is eliminated by erasure + // it's actually wrong (in AST, readVal(_:BoundVal) adds a Rep wrapper! + //hopHole(name, typ, vars, visible.toSet -- vars.flatten toList) + throw new UnsupportedOperationException("Higher-order patterns") + } /** Pattern hole in type position */ def typeHole(name: String): TypeRep @@ -62,6 +72,8 @@ self: Base => /* if (defs isEmpty) r else */ // <- This "optimization" is not welcome, as some IRs (ANF) may relie on `substitute` being called for all insertions substitute(r, defs.toMap) + final def insertAfterTransformation(r: => Rep, defs: (String, Rep)*): Rep = insertAfterTransformation(r, defs.toMap) + protected def mkIR[T,C](r: Rep): IR[T,C] = new IR[T,C] { val rep = r override def equals(that: Any): Boolean = that match { @@ -291,6 +303,8 @@ self: Base => res } + protected def mergeAll(as: Extract*): Option[Extract] = mergeAll(as.map(Some(_))) + def mergeableReps(a: Rep, b: Rep): Boolean = a =~= b def mergeTypes(a: TypeRep, b: TypeRep): Option[TypeRep] = @@ -320,11 +334,14 @@ self: Base => def $Code[T](q: Code[T]): T = ??? // TODO B/E -- also, rename to 'unquote'? def $Code[A,B](q: Code[A] => Code[B]): A => B = ??? - /* To support hole syntax `xs?` (old syntax `$$xs`) (in ction) or `$xs` (in xtion) */ + /* To support hole syntax `?xs` (old syntax `$$xs`) (in ction) or `$xs` (in xtion) */ def $$[T](name: Symbol): T = ??? /* To support hole syntax `$$xs: _*` (in ction) or `$xs: _*` (in xtion) */ def $$_*[T](name: Symbol): Seq[T] = ??? + /** Higher-order pattern */ + def $$_hop[T](name: Symbol)(args: Any*): T = ??? + implicit def liftFun[A:IRType,B,C](qf: IR[A,C] => IR[B,C]): IR[A => B,C] = { val bv = bindVal("lifted", typeRepOf[A], Nil) // add TODO annotation recording the lifting? diff --git a/core/src/main/scala/squid/quasi/QuasiEmbedder.scala b/core/src/main/scala/squid/quasi/QuasiEmbedder.scala index 7bcf78ba..85678543 100644 --- a/core/src/main/scala/squid/quasi/QuasiEmbedder.scala +++ b/core/src/main/scala/squid/quasi/QuasiEmbedder.scala @@ -42,13 +42,11 @@ class QuasiEmbedder[C <: whitebox.Context](val c: C) { /** holes: Seq(either term or type); splicedHoles: which holes are spliced term holes (eg: List($xs*)) * holes in ction mode are interpreted as free variables (and are never spliced) - * unquotedTypes contains the types unquoted in (in ction mode with $ and in xtion mode with $$) - * TODO: actually use `unquotedTypes`! - * TODO maybe this should go in QuasiMacros... */ - def applyQQ(Base: Tree, tree: c.Tree, holes: Seq[Either[TermName,TypeName]], - splicedHoles: collection.Set[TermName], hopvHoles: collection.Map[TermName,List[List[TermName]]], - typeBounds: Map[TypeName,EitherOrBoth[Tree,Tree]], - unquotedTypes: Seq[(TypeName, Type, Tree)], unapply: Option[c.Tree], config: QuasiConfig): c.Tree = { + * unquotedTypes contains the types that are inserted (in ction mode with $ and in xtion mode with $$) */ + def applyQQ(Base: Tree, tree: c.Tree, holes: Seq[Either[TermName,TypeName]], + splicedHoles: collection.Set[TermName], hopHoles: collection.Set[TermName], + typeBounds: Map[TypeName,EitherOrBoth[Tree,Tree]], + unquotedTypes: Seq[(TypeName, Type, Tree)], unapply: Option[c.Tree], config: QuasiConfig): c.Tree = { //debug("HOLES:",holes) @@ -226,7 +224,7 @@ class QuasiEmbedder[C <: whitebox.Context](val c: C) { apply(Base, finalTree, termScope, config, unapply, - typeSymbols, holeSymbols, holes, splicedHoles, hopvHoles, termHoles, typeHoles, typedTree, typedTreeType, stmts, convNames, unquotedTypes) + typeSymbols, holeSymbols, holes, splicedHoles, hopHoles, termHoles, typeHoles, typedTree, typedTreeType, stmts, convNames, unquotedTypes) } @@ -242,7 +240,7 @@ class QuasiEmbedder[C <: whitebox.Context](val c: C) { def apply( baseTree: Tree, rawTree: c.Tree, termScopeParam: List[Type], config: QuasiConfig, unapply: Option[c.Tree], typeSymbols: Map[TypeName, Symbol], holeSymbols: Set[Symbol], - holes: Seq[Either[TermName,TypeName]], splicedHoles: collection.Set[TermName], hopvHoles: collection.Map[TermName,List[List[TermName]]], + holes: Seq[Either[TermName,TypeName]], splicedHoles: collection.Set[TermName], hopHoles: collection.Set[TermName], termHoles: Set[TermName], typeHoles: Set[TypeName], typedTree: Tree, typedTreeType: Type, stmts: List[Tree], convNames: Set[TermName], unquotedTypes: Seq[(TypeName, Type, Tree)] ): c.Tree = { @@ -322,6 +320,23 @@ class QuasiEmbedder[C <: whitebox.Context](val c: C) { override def liftTerm(x: Tree, parent: Tree, expectedType: Option[Type], inVarargsPos: Boolean)(implicit ctx: Map[TermSymbol, b.BoundVal]): b.Rep = { object HoleName { def unapply(tr: Tree) = Some(holeName(tr,x)) } + def mkHoleType(name: String, tpt: Tree) = expectedType match { + case None if tpt.tpe <:< Nothing => throw EmbeddingException(s"No type info for hole '$name'" + ( + if (debug.debugOptionEnabled) s", in: $parent" else "" )) // TODO: check only after we have explored all repetitions of the hole? (not sure if possible) + case Some(tp) => + assert(!SCALA_REPEATED(tp.typeSymbol.fullName.toString)) + + if (tp <:< Nothing) parent match { + case q"$_: $_" => // this hole is ascribed explicitly; the Nothing type must be desired + case _ => macroContext.warning(macroContext.enclosingPosition, + s"Type inferred for hole '$name' was Nothing. Ascribe the hole explicitly to remove this warning.") // TODO show real hole in its original form + } + + val mergedTp = if (tp <:< tpt.tpe || tpt.tpe <:< Nothing) tp else tpt.tpe + debug(s"[Hole '$name'] Expected",tp," required",tpt.tpe," merged",mergedTp) + + mergedTp + } x match { @@ -465,74 +480,57 @@ class QuasiEmbedder[C <: whitebox.Context](val c: C) { throw QuasiException("Unknown use of free variable syntax operator `?`.") + /** Handling of higher-order patterns: */ + case q"$baseTree.$$$$_hop[$tpt](${nameTree @ HoleName(name)})(..$args)" => // TODO check baseTree + // TODO remove _extracted_ binders (as in `val $x = ...`) from 'visible' bindings?; add them to context requirements... + debug("HOP",tpt,nameTree,args) + + val holeType = mkHoleType(name, tpt) + + // TODO handle contexts and FVs... + // TODO make sure HOPV holes with the same name are not repeated? or at least have the same param types + // Q: handle HOPV holes in spliced position? + + val termName = TermName(name) + + val hopvType = FunctionType(args map (_.tpe) : _*)(holeType) + termHoleInfo(termName) = Map() -> hopvType + + val largs = args map (arg => b.byName(liftTerm(arg,x,None,false))) + + b.hopHole2(name, liftType(holeType), (largs)::Nil, ctx.values.toList) + /** Replaces calls to $$(name) with actual holes */ case q"$baseTree.$$$$[$tpt]($nameTree)" => // TODO check baseTree val name = holeName(nameTree, x) - val holeType = expectedType match { - case None if tpt.tpe <:< Nothing => throw EmbeddingException(s"No type info for hole '$name'" + ( - if (debug.debugOptionEnabled) s", in: $parent" else "" )) // TODO: check only after we have explored all repetitions of the hole? (not sure if possible) - case Some(tp) => - assert(!SCALA_REPEATED(tp.typeSymbol.fullName.toString)) - - if (tp <:< Nothing) parent match { - case q"$_: $_" => // this hole is ascribed explicitly; the Nothing type must be desired - case _ => macroContext.warning(macroContext.enclosingPosition, - s"Type inferred for hole '$name' was Nothing. Ascribe the hole explicitly to remove this warning.") // TODO show real hole in its original form - } - - val mergedTp = if (tp <:< tpt.tpe || tpt.tpe <:< Nothing) tp else tpt.tpe - debug(s"[Hole '$name'] Expected",tp," required",tpt.tpe," merged",mergedTp) - - mergedTp - } - + val holeType = mkHoleType(name, tpt) //if (splicedHoles(TermName(name))) throw EmbeddingException(s"Misplaced spliced hole: '$name'") // used to be: q"$Base.splicedHole[${_expectedType}](${name.toString})" val termName = TermName(name) - hopvHoles get TermName(name) match { - case Some(idents) => - - // TODO make sure HOPV holes withb the same name are not repeated? or at least have the same param types - // TODO handle HOPV holes in spliced position? - - val scp = ctx map { case k -> v => k.name -> (k -> v) } - val identVals = idents map (_ map scp) - val yes = identVals map (_ map (_._2)) - val keys = idents.flatten.toSet - val no = scp.filterKeys(!keys(_)).values.map(_._2).toList - debug(s"HOPV: yes=$yes; no=$no") - val List(identVals2) = identVals // FIXME generalize - val hopvType = FunctionType(identVals2 map (_._1.typeSignature) : _*)(holeType) - termHoleInfo(termName) = Map() -> hopvType - b.hopHole(name, liftType(holeType), yes, no) - - case _ => - if (unapply isEmpty) { - debug(s"Free variable: $name: $tpt") - freeVariableInstances ::= name -> holeType - // TODO maybe aggregate glb type beforehand so we can pass the precise type here... could even pass the _same_ hole! - } else { - //val termName = TermName(name) - val scp = ctx.keys map (k => k.name -> k.typeSignature) toMap; - termHoleInfo get termName map { case (scp0, holeType0) => - val newScp = scp0 ++ scp.map { case (n,t) => lub(t :: scp0.getOrElse(n, Any) :: Nil) } - newScp -> lub(holeType :: holeType0 :: Nil) - } getOrElse { - termHoleInfo(termName) = scp -> holeType - } - } - - if (splicedHoles(TermName(name))) - b.splicedHole(name, liftType(holeType)) - else b.hole(name, liftType(holeType)) - + if (unapply isEmpty) { + debug(s"Free variable: $name: $tpt") + freeVariableInstances ::= name -> holeType + // TODO maybe aggregate glb type beforehand so we can pass the precise type here... could even pass the _same_ hole! + } else { + //val termName = TermName(name) + val scp = ctx.keys map (k => k.name -> k.typeSignature) toMap; + termHoleInfo get termName map { case (scp0, holeType0) => + val newScp = scp0 ++ scp.map { case (n,t) => lub(t :: scp0.getOrElse(n, Any) :: Nil) } + newScp -> lub(holeType :: holeType0 :: Nil) + } getOrElse { + termHoleInfo(termName) = scp -> holeType + } } + if (splicedHoles(TermName(name))) + b.splicedHole(name, liftType(holeType)) + else b.hole(name, liftType(holeType)) + /** Removes implicit conversions that were generated in order to apply the subtyping knowledge extracted from pattern matching */ case q"${Ident(name:TermName)}.apply($x)" if convNames(name) => liftTerm(x, parent, typeIfNotNothing(x.tpe)) @@ -642,7 +640,7 @@ class QuasiEmbedder[C <: whitebox.Context](val c: C) { val typeTypesToExtract = typeSymbols mapValues { sym => tq"$baseTree.IRType[$sym]" } val extrTyps = holes.map { - case Left(vname) => termTypesToExtract(vname) + case Left(vname) => termTypesToExtract(vname) // TODO B/E case Right(tname) => typeTypesToExtract(tname) // TODO B/E } debug("Extracted Types: "+extrTyps.map(showCode(_)).mkString(", ")) diff --git a/core/src/main/scala/squid/quasi/QuasiMacros.scala b/core/src/main/scala/squid/quasi/QuasiMacros.scala index 3919a05b..b163e107 100644 --- a/core/src/main/scala/squid/quasi/QuasiMacros.scala +++ b/core/src/main/scala/squid/quasi/QuasiMacros.scala @@ -106,7 +106,7 @@ class QuasiMacros(val c: whitebox.Context) { holeSymbols = Set(), holes = Seq(), splicedHoles = Set(), - hopvHoles = Map(), + hopHoles = Set(), termHoles = Set(), typeHoles = Set(), typedTree = code, @@ -200,7 +200,7 @@ class QuasiMacros(val c: whitebox.Context) { var holes: List[(Either[TermName,TypeName], Tree)] = Nil // (Left(value-hole) | Right(type-hole), original-hole-tree) val splicedHoles = mutable.Set[TermName]() - val hopvHoles = mutable.Map[TermName,List[List[TermName]]]() + val hopvHoles = mutable.Set[TermName]() var typeBounds: List[(TypeName, EitherOrBoth[Tree,Tree])] = Nil // Keeps track of which holes still have not been found in the source code @@ -302,18 +302,17 @@ class QuasiMacros(val c: whitebox.Context) { case Ident(name: TermName) if builder.holes.contains(name) => mkTermHole(name, false) - // Identify and treat Higher-Order Pattern Variables (HOPV) - case q"${Ident(name: TermName)}(...$argss)" if isUnapply && builder.holes.contains(name) => - val idents = argss map (_ map { - case Ident(name:TermName) => name - case e => throw EmbeddingException(s"Unexpected expression in higher-order pattern variable argument: ${showCode(e)}") - }) - val hole = builder.holes(name) - val n = hole.name filter (_.toString != "_") getOrElse ( + // Identify and treat Higher-Order Patterns (HOP) + case q"${Ident(nme: TermName)}(...$argss)" if isUnapply && builder.holes.contains(nme) => + val List(args) = argss // TODO handle ...$argss + val hole = builder.holes(nme) + val name = hole.name filter (_.toString != "_") getOrElse ( throw QuasiException("All higher-order holes should be named.") // Q: necessary restriction? ) toTermName; - hopvHoles += n -> idents - mkTermHole(name, false) + hopvHoles += name + remainingHoles -= nme + holes ::= Left(name) -> hole.tree + q"$base.$$$$_hop(${Symbol(name toString)})(..$args)" // Interprets bounds on extracted types, like in: `case List[$t where (Null <:< t <:< AnyRef)]`: case tq"${Ident(name: TypeName)} where $bounds" if isUnapply && builder.holes.contains(name.toTermName) => diff --git a/src/main/scala/squid/ir/AST.scala b/src/main/scala/squid/ir/AST.scala index 9a47fceb..773f2982 100644 --- a/src/main/scala/squid/ir/AST.scala +++ b/src/main/scala/squid/ir/AST.scala @@ -298,6 +298,12 @@ trait AST extends InspectableBase with ScalaTyping with ASTReinterpreter with Ru override def hopHole(name: String, typ: TypeRep, yes: List[List[Val]], no: List[Val]) = rep(new HOPHole(name, typ, yes, no)) class HOPHole(name: String, typ: TypeRep, val yes: List[List[Val]], val no: List[Val]) extends HoleClass(name, typ)() + override def hopHole2(name: String, typ: TypeRep, args: List[List[Rep]], visible: List[BoundVal]): Rep = { + // TODO replace `hopHole` + val vars = args map (_ map (r => dfn(r).asInstanceOf[BoundVal])) + hopHole(name, typ, vars, visible.toSet -- vars.flatten toList) + } + case class Constant(value: Any) extends Def { lazy val typ = value match { case () => TypeRep(ruh.Unit) diff --git a/src/main/scala/squid/ir/fastanf/Effects.scala b/src/main/scala/squid/ir/fastanf/Effects.scala new file mode 100644 index 00000000..00e69845 --- /dev/null +++ b/src/main/scala/squid/ir/fastanf/Effects.scala @@ -0,0 +1,95 @@ +package squid.ir.fastanf + +import squid.utils.Bool + +import scala.collection.mutable + +/** + * Basic effect system where statements can be `Pure` or `Impure`. + * By default everything is impure. + */ +trait Effects { + private val pureMtds = mutable.Set[MethodSymbol]() + //protected val pureTyps = mutable.Set[TypeSymbol]() + + def addPureMtd(m: MethodSymbol): Unit = pureMtds += m + //def addPureTyp(t: TypeSymbol): Unit = pureTyps += t + + def isPure(r: Rep): Boolean = effect(r) == Pure + def isPure(d: Def): Boolean = defEffect(d) == Pure + + def effect(d: Def): Effect = defEffect(d) + + def effect(r: Rep): Effect = r match { + case lb: LetBinding => defEffect(lb.value) |+| effect(lb.body) + + //case s: Symbol => s.owner match { + // case lb: LetBinding => effect(lb) + // case _ => Pure + //} + case _: Symbol => Pure + + case ByName(r) => effect(r) + + case Ascribe(r, _) => effect(r) + + case Module(r, _, _) => effect(r) + + case HOPHole2(_, _, args, _) => Impure//args.flatten.foldLeft(Pure: Effect) { _ |+| effect(_) } + + case Constant(_) | _: Symbol | + StaticModule(_) | NewObject(_) | + Hole(_, _) | SplicedHole(_, _) | + HOPHole(_, _, _, _) => Pure + } + + def mtdEffect(m: MethodSymbol): Effect = if (pureMtds contains m) Pure else Impure + + def defEffect(d: Def): Effect = d match { + case l: Lambda => effect(l.body) + case ma: MethodApp => + val selfEff = effect(ma.self) + val argssEff = ma.argss.argssList.map(effect).fold(Pure)(_ |+| _) + val mtdEff = mtdEffect(ma.mtd) + selfEff |+| argssEff |+| mtdEff + case DefHole(_) => Impure + } + + sealed trait Effect { + /** + * Combine effects. + */ + def |+|(e: Effect): Effect + } + + case object Pure extends Effect { + def |+|(e: Effect): Effect = e + } + + case object Impure extends Effect { + def |+|(e: Effect): Effect = Impure + } +} + +/** + * Standard effect system. + * Defines the set of pure methods used in tests. + */ +trait StandardEffects extends Effects { + addPureMtd(MethodSymbol(TypeSymbol("scala.Int"),"$plus")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Double"),"$plus")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Int"),"$times")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Double"),"$times")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Double"),"div")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Int"), "toDouble")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Double"), "toInt")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Int"), "toFloat")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Option$"), "apply")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Option"), "get")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Tuple2$"), "apply")) + addPureMtd(MethodSymbol(TypeSymbol("scala.Tuple3$"), "apply")) + addPureMtd(MethodSymbol(TypeSymbol("squid.lib.package$"),"uncurried2")) + addPureMtd(MethodSymbol(TypeSymbol("scala.collection.LinearSeqOptimized"),"foldLeft")) + addPureMtd(MethodSymbol(TypeSymbol("scala.collection.immutable.List$"),"apply")) + addPureMtd(MethodSymbol(TypeSymbol("scala.collection.immutable.List$"),"canBuildFrom")) +} diff --git a/src/main/scala/squid/ir/fastanf/FastANF.scala b/src/main/scala/squid/ir/fastanf/FastANF.scala index 693e053e..74b9c1c6 100644 --- a/src/main/scala/squid/ir/fastanf/FastANF.scala +++ b/src/main/scala/squid/ir/fastanf/FastANF.scala @@ -1,12 +1,17 @@ package squid package ir.fastanf -import utils._ -import lang.{InspectableBase, Base} -import squid.ir.CurryEncoding -import squid.lang.ScalaCore +import squid.ir._ +import squid.lang.{Base, InspectableBase, ScalaCore} +import squid.utils._ -class FastANF extends InspectableBase with CurryEncoding with ScalaCore { +import scala.collection.immutable.{ListMap, ListSet} + +/** + * ANF representation of the code. + * The IR is mutable in order to be able to do O(1) substitutions. + */ +class FastANF extends InspectableBase with CurryEncoding with StandardEffects with ScalaCore { private[this] implicit val base = this @@ -22,7 +27,11 @@ class FastANF extends InspectableBase with CurryEncoding with ScalaCore { // * --- * --- * --- * Reification * --- * --- * --- * var scopes: List[ReificationContext] = Nil - + + /** + * Runs the thunk `r`, appends it to the the last let-binding in the reification context, + * and returns the first let-binding. + */ @inline final def wrap(r: => Rep, inXtor: Bool): Rep = { val scp = new ReificationContext(inXtor) scopes ::= scp @@ -37,11 +46,46 @@ class FastANF extends InspectableBase with CurryEncoding with ScalaCore { @inline final def currentScope = scopes.head - // TODO make squid.lang.Base and QQs agnostic in the argument list impl to avoid this conversion! def toArgumentLists(argss: List[ArgList]): ArgumentLists = { + // Note: some arguments may be let-bindings (ie: blocks), which is only possible if they are by-name arguments + + def toArgumentList(args: Seq[Rep]): ArgumentList = + args.foldRight(NoArguments: ArgumentList)(_ ~: _) + def toArgumentListWithSpliced(args: Seq[Rep])(splicedArg: Rep) = + args.foldRight(SplicedArgument(splicedArg): ArgumentList)(_ ~: _) + + argss.foldRight(NoArgumentLists: ArgumentLists) { + (args, acc) => args match { + case Args(as @ _*) => toArgumentList(as) ~~: acc + case ArgsVarargs(Args(as @ _*), Args(bs @ _*)) => toArgumentList(as ++ bs) ~~: acc // ArgVararg ~converted as Args! + case ArgsVarargSpliced(Args(as @ _*), s) => toArgumentListWithSpliced(as)(s) ~~: acc + } + } + } + + def toListOfArgList(argss: ArgumentLists): List[ArgList] = { + def toArgList(args: ArgumentList): List[Rep] -> Option[Rep] = args match { + case NoArguments => Nil -> None + case SplicedArgument(a) => Nil -> Some(a) // Everything after spliced argument is ignored. + case r : Rep => List(r) -> None + case ArgumentCons(h, t) => + val (rest, spliced) = toArgList(t) + (h :: rest) -> spliced + } + argss match { - case Nil => NoArgumentLists - case Args(a) :: Nil => a + case ArgumentListCons(h, t) => + val (args, spliced) = toArgList(h) + val _args = Args(args: _*) + spliced.fold(_args: ArgList)(s => ArgsVarargSpliced(_args, s)) :: toListOfArgList(t) + case NoArgumentLists => Nil + case SplicedArgument(spliced) => List(ArgsVarargSpliced(Args(), spliced)) // Not sure + case ac : ArgumentCons => + val (args, spliced) = toArgList(ac) + val _args = Args(args: _*) + spliced.fold(_args: ArgList)(s => ArgsVarargSpliced(_args, s)) :: Nil + case NoArguments => Nil + case r : Rep => List(Args(r)) } } @@ -49,7 +93,7 @@ class FastANF extends InspectableBase with CurryEncoding with ScalaCore { // * --- * --- * --- * Implementations of `Base` methods * --- * --- * --- * def bindVal(name: String, typ: TypeRep, annots: List[Annot]): BoundVal = new UnboundSymbol(name,typ) - def readVal(bv: BoundVal): Rep = bv + def readVal(bv: BoundVal): Rep = curSub getOrElse (bv, bv) def const(value: Any): Rep = Constant(value) // Note: method `lambda(params: List[BoundVal], body: => Rep): Rep` is implemented by CurryEncoding @@ -61,22 +105,122 @@ class FastANF extends InspectableBase with CurryEncoding with ScalaCore { def lambdaType(paramTyps: List[TypeRep], ret: TypeRep): TypeRep = DummyTypeRep def staticModule(fullName: String): Rep = StaticModule(fullName) - def module(prefix: Rep, name: String, typ: TypeRep): Rep = unsupported - def newObject(tp: TypeRep): Rep = unsupported - def methodApp(self: Rep, mtd: MtdSymbol, targs: List[TypeRep], argss: List[ArgList], tp: TypeRep): Rep = { - MethodApp(self, mtd, targs, argss |> toArgumentLists, tp) |> letbind + def module(prefix: Rep, name: String, typ: TypeRep): Rep = Module(prefix, name, typ) + def newObject(typ: TypeRep): Rep = NewObject(typ) + def methodApp(self: Rep, mtd: MtdSymbol, targs: List[TypeRep], argss: List[ArgList], tp: TypeRep): Rep = mtd match { + // Converts the call to `Imperative` to let-bindings + case MethodSymbol(TypeSymbol("squid.lib.package$"), "Imperative") => argss match { + case List(h, t) => + val holes = h.reps.filter { + case Hole(_, _) => true + case _ => false + } + + val lastArgss = t.reps + assert(lastArgss.size == 1) + holes.foldRight(lastArgss.head) { case (h, acc) => + letin(bindVal("tmp", h.typ, Nil), h, acc, acc.typ) + } + } + + case _ => MethodApp(self |> inlineBlock, mtd, targs, argss |> toArgumentLists, tp) |> letbind } - def byName(mkArg: => Rep): Rep = wrapNest(mkArg) - + def byName(mkArg: => Rep): Rep = ByName(wrapNest(mkArg)) + + /** + * Let-bind `d` and add it the current reification scope. + */ def letbind(d: Def): Rep = currentScope += d - + + /** + * Adds all the statements of `r` to the current reification context. + * Returns the final non-statement unchanged. + */ + def inlineBlock(r: Rep): Rep = r |>=? { + case lb: LetBinding => + currentScope += lb + inlineBlock(lb.body) + } + + /** + * Let binds `value` to `bound` in the `body`. + */ + override def letin(bound: BoundVal, value: Rep, body: => Rep, bodyType: TypeRep): Rep = value match { + case s: Symbol => + s.owner |>? { + case lb: RebindableBinding => + lb.name = bound.name + } + s.owner |>? { + case lb: LetBinding => + lb.isUserDefined = true + } + withSubs(bound, value)(body) + + //s.owner |>? { + // case lb: RebindableBinding => + // lb.name = bound.name + //} + //bound rebind s + //body + + case lb: LetBinding => + // conceptually, does like `inlineBlock`, but additionally rewrites `bound` and renames `lb`'s last binding + val last = lb.last + val boundName = bound.name + bound rebind last.bound + last.body = body + last.name = boundName // TODO make sure we're only renaming an automatically-named binding? + lb + + case h: Hole => + val dh = DefHole(h) |> letbind + withSubs(bound -> dh)(body) + + //(dh |>? { + // case bv: BoundVal => bv.owner |>? { + // case lb: LetBinding => + // lb.body = body + // lb + // } + //}).flatten.getOrElse(body) + + //new LetBinding(bound.name, bound, dh, body) alsoApply (currentScope += _) alsoApply (bound.rebind) + + case (_:HOPHole) | (_:HOPHole2) | (_:SplicedHole) => + ??? // TODO holes should probably be Def's; note that it's not safe to do a substitution for holes + case _ => + withSubs(bound -> value)(body) + // ^ executing `body` will reify some statements into the reification scope, and likely return a symbol + // during this reification, we need all references to `bound` to be replaced by the actual `value` + } + + var curSub: Map[Symbol,Rep] = Map.empty + + /** + * Substitutes the [[Symbol]]s in `k` with the based on the mappings in [[curSub]] and `subs`. + */ + def withSubs[R](subs: Symbol -> Rep)(k: => R): R = { + val oldSub = curSub + curSub += subs + try k finally curSub = oldSub + } + + override def tryInline(fun: Rep, arg: Rep)(retTp: TypeRep): Rep = fun match { + case lb: LetBinding => lb.value match { + case l: Lambda => letin(l.bound, arg, l.body, l.body.typ) + case _ => super.tryInline(fun, arg)(retTp) + } + case _ => super.tryInline(fun, arg)(retTp) + } + override def ascribe(self: Rep, typ: TypeRep): Rep = if (self.typ =:= typ) self else self match { case Ascribe(trueSelf, _) => Ascribe(trueSelf, typ) // Hopefully Scala's subtyping is transitive! case _ => Ascribe(self, typ) } - - def loadMtdSymbol(typ: TypSymbol, symName: String, index: Option[Int] = None, static: Boolean = false): MtdSymbol = new MethodSymbol(symName) // TODO - + + def loadMtdSymbol(typ: TypSymbol, symName: String, index: Option[Int] = None, static: Boolean = false): MtdSymbol = MethodSymbol(typ, symName) // TODO + object Const extends ConstAPI { def unapply[T: IRType](ir: IR[T,_]): Option[T] = ir.rep match { case cst @ Constant(v) if typLeq(cst.typ, irTypeOf[T].rep) => Some(v.asInstanceOf[T]) @@ -84,52 +228,929 @@ class FastANF extends InspectableBase with CurryEncoding with ScalaCore { } } - def repEq(a: Rep, b: Rep): Boolean = a === b // FIXME impl term equivalence - - + def repEq(a: Rep, b: Rep): Boolean = + (a extractRep b) === Some(EmptyExtract) && (b extractRep a) === Some(EmptyExtract) + + // * --- * --- * --- * Implementations of `IntermediateBase` methods * --- * --- * --- * - - def nullValue[T: IRType]: IR[T,{}] = unsupported - def reinterpret(r: Rep, newBase: Base)(extrudedHandle: BoundVal => newBase.Rep): newBase.Rep = unsupported + + def nullValue[T: IRType]: IR[T,{}] = IR[T, {}](const(null)) // FIXME: should implement proper semantics; e.g. nullValue[Int] == ir"0", not ir"null" + def reinterpret(r: Rep, newBase: Base)(extrudedHandle: BoundVal => newBase.Rep): newBase.Rep = { + def reinterpret0: Rep => newBase.Rep = r => reinterpret(r, newBase)(extrudedHandle) + def reinterpretType: TypeRep => newBase.TypeRep = t => newBase.staticTypeApp(newBase.loadTypSymbol("scala.Any"), Nil) + def reinterpretBV:BoundVal => newBase.BoundVal = bv => newBase.bindVal(bv.name, reinterpretType(bv.typ), Nil) + def reinterpretTypSym(t: TypeSymbol): newBase.TypSymbol = newBase.loadTypSymbol(t.name) + def reinterpretMtdSym(s: MtdSymbol): newBase.MtdSymbol = newBase.loadMtdSymbol(reinterpretTypSym(s.typ), s.name) + def reinterpretArgList(argss: ArgumentLists): List[newBase.ArgList] = toListOfArgList(argss) map { + case ArgsVarargSpliced(args, varargs) => newBase.ArgsVarargSpliced(args.map(newBase)(reinterpret0), reinterpret0(varargs)) + case ArgsVarargs(args, varargs) => newBase.ArgsVarargs(args.map(newBase)(reinterpret0), varargs.map(newBase)(reinterpret0)) + case args : Args => args.map(newBase)(reinterpret0) + } + def defToRep(d: Def): newBase.Rep = d match { + case app @ App(f, a) => newBase.app(reinterpret0(f), reinterpret0(a))(reinterpretType(app.typ)) + case ma : MethodApp => newBase.methodApp( + reinterpret0(ma.self), + reinterpretMtdSym(ma.mtd), + ma.targs.map(reinterpretType), + reinterpretArgList(ma.argss), + reinterpretType(ma.typ)) + case l: Lambda => newBase.lambda(List(reinterpretBV(l.bound)), reinterpret0(l.body)) + case DefHole(h) => newBase.hole(h.name, reinterpretType(h.typ)) + } + + r match { + case Constant(v) => newBase.const(v) + case StaticModule(fn) => newBase.staticModule(fn) + case NewObject(t) => newBase.newObject(reinterpretType(t)) + case Hole(n, t) => newBase.hole(n, reinterpretType(t)) + case SplicedHole(n, t) => newBase.splicedHole(n, reinterpretType(t)) + case Ascribe(s, t) => newBase.ascribe(reinterpret0(s), reinterpretType(t)) + case HOPHole(n, t, args, visible) => newBase.hopHole( + n, + reinterpretType(t), + args.map(_.map(reinterpretBV)), + visible.map(reinterpretBV)) + case HOPHole2(n, t, args, visible) => newBase.hopHole2( + n, + reinterpretType(t), + args.map(_.map(reinterpret0)), + visible.map(reinterpretBV) + ) + case Module(p, n, t) => newBase.module(reinterpret0(p), n, reinterpretType(t)) + case lb: LetBinding => newBase.letin( + reinterpretBV(lb.bound), + defToRep(lb.value), + reinterpret0(lb.body), + reinterpretType(lb.typ)) + case s: Symbol => extrudedHandle(s) + case ByName(r) => newBase.byName(reinterpret0(r)) + } + + } def repType(r: Rep): TypeRep = r.typ - def boundValType(bv: BoundVal) = bv.typ + def boundValType(bv: BoundVal): TypeRep = bv.typ // * --- * --- * --- * Implementations of `InspectableBase` methods * --- * --- * --- * - - def extractType(xtor: TypeRep, xtee: TypeRep,va: squid.ir.Variance): Option[Extract] = unsupported + + def extractType(xtor: TypeRep, xtee: TypeRep, va: squid.ir.Variance): Option[Extract] = Some(EmptyExtract) //unsupported def bottomUp(r: Rep)(f: Rep => Rep): Rep = transformRep(r)(identity, f) def topDown(r: Rep)(f: Rep => Rep): Rep = transformRep(r)(f) - def transformRep(r: Rep)(pre: Rep => Rep, post: Rep => Rep = identity): Rep = unsupported - protected def extract(xtor: Rep, xtee: Rep): Option[Extract] = unsupported - protected def spliceExtract(xtor: Rep, args: Args): Option[Extract] = unsupported + def transformRep(r: Rep)(pre: Rep => Rep, post: Rep => Rep = identity): Rep = { + def transformRep0(r: Rep) = transformRep(r)(pre, post) + + def transformDef(d: Def): Either[Rep, Def] = d match { + case ma: MethodApp => + Left(MethodApp.toANF(transformRep0(ma.self), ma.mtd, ma.targs, ma.argss argssMap transformRep0, ma.typ)) + case l: Lambda => + l.body = l.body |> transformRep0 + Right(l) + case _ => Right(d) + } + + post(pre(r) match { + case lb: LetBinding => + lb.value |> transformDef match { + case Right(d) => + lb.value = d + lb.body = lb.body |> transformRep0 + lb + case Left(r) => LetBinding.withRepValue(lb.name, lb.bound, r, lb.body |> transformRep0) + } + case ByName(r) => ByName(transformRep0(r)) + case Ascribe(s, t) => Ascribe(transformRep0(s), t) + case Module(p, n, t) => Module(transformRep0(p), n, t) + case r @ (_: Constant | _: Hole | _: Symbol | _: SplicedHole | _: HOPHole | _: HOPHole2 | _: NewObject | _: StaticModule) => r + }) + } + + protected def extract(xtor: Rep, xtee: Rep): Option[Extract] = extractWithState(xtor, xtee)(State.forExtraction(xtor, xtee)).toOption map (_.ex) + + // Context is mapping from xtor BVs to xtee BVs + type Ctx = Map[BoundVal, BoundVal] + + // * --- * --- * --- * Extraction State * --- * --- * --- * + + /** + * Mapping of failed matches between xtor BVs and xtee BVs. + */ + type Failed = Map[BoundVal, Set[BoundVal]] - // * --- * --- * --- * Implementations of `QuasiBase` methods * --- * --- * --- * + /** + * Signals if the current extraction attempt has failed. + * `Left` returns the matchings that failed (xtor -> xtee), + * `Right` the update state after a successful extraction. + */ + type ExtractState = Either[Set[(BoundVal, BoundVal)], State] + + /* Helper functions for `ExtractState` */ + def succeed(implicit es: State): ExtractState = Right(es) + def fail = Left(Set.empty[(BoundVal, BoundVal)]) + def failWith(failed: Set[(BoundVal, BoundVal)]): ExtractState = Left(failed) - def hole(name: String, typ: TypeRep) = unsupported - def splicedHole(name: String, typ: TypeRep): Rep = unsupported - def typeHole(name: String): TypeRep = unsupported - def hopHole(name: String, typ: TypeRep, yes: List[List[BoundVal]], no: List[BoundVal]) = unsupported + implicit def rightBias[A, B](e: Either[A, B]): Either.RightProjection[A,B] = e.right + + /** + * Represents the current of the state of the extraction. + * @param ex what has been extracted by holes. + * @param ctx discovered matchings between bound values in the `xtor` and the `xtee`. + * @param instructions what has to be done for each let-binding + * @param matchedImpureBVs impure statements that have been matched + * @param failed statements that do not match + * @param strategy see [[Strategy]] + */ + case class State(ex: Extract, ctx: Ctx, instructions: Instructions, matchedImpureBVs: Set[BoundVal], + failed: Failed, strategy: Strategy) { + private val _strategy = strategy + private val _instructions = instructions + + def withNewExtract(newEx: Extract): State = copy(ex = newEx) + def withCtx(newCtx: Ctx): State = copy(ctx = newCtx) + def withCtx(p: (BoundVal, BoundVal)): State = copy(ctx = ctx + p) + + /** + * Adds all the BVs referencing impure statements in `r` to `matchedImpureBVs`. + */ + def withMatchedImpures(r: Rep): State = r match { + case lb: LetBinding if !isPure(lb.value) => copy(matchedImpureBVs = matchedImpureBVs + lb.bound) withMatchedImpures lb.body + case lb: LetBinding => this withMatchedImpures lb.body + case _ => this // Everything else is pure so we ignore it + } + def withMatchedImpure(bv: BoundVal): State = copy(matchedImpureBVs = matchedImpureBVs + bv) + + def withFailed(newFailed: Set[(BoundVal, BoundVal)]): State = { + val updatedFailed = newFailed.foldLeft(failed) { case (mergedF, (k, v)) => + mergedF + (k -> mergedF.get(k).map(_ + v).getOrElse(Set(v))) + } + copy(failed = updatedFailed) + } + + def withStrategy(s: Strategy): State = copy(strategy = s) + def withDefaultStrategy: State = copy(strategy = _strategy) + + def withInstructionsFor(r: Rep): State = copy(instructions = instructions.copy(flags = instructions.flags ++ Instructions.gen(r))) + def withDefaultInstructions: State = copy(instructions = _instructions) + + def updateExtractWith(e: Option[Extract]*)(implicit default: State): ExtractState = { + mergeAll(Some(ex) +: e).fold[ExtractState](fail)(ex => succeed(this withNewExtract ex)) + } + } - def substitute(r: => Rep, defs: Map[String, Rep]): Rep = unsupported + object State { + def forExtraction(xtor: Rep, xtee: Rep): State = apply(xtor, xtee, CompleteMatching) + def forRewriting(xtor: Rep, xtee: Rep): State = apply(xtor, xtee, PartialMatching) + + private def apply(xtor: Rep, xtee: Rep, strategy: Strategy): State = + State(EmptyExtract, ListMap.empty, Instructions(xtor, xtee), Set.empty, Map.empty, strategy) + } + + + + // * --- * --- * --- * Strategy * --- * --- * --- * + + /** + * Specifies the semantics of the extraction. + */ + sealed trait Strategy + + /** + * Allows the return value of the `xtor` to match anywhere in `xtee`. + * This will potentially leave some of the statement of the `xtee` unmatched. + * For instance, this [[Strategy]] is necessary for rewriting as we may only be rewriting parts of the `xtee`. + */ + case object PartialMatching extends Strategy - // * --- * --- * --- * Implementations of `TypingBase` methods * --- * --- * --- * + /** + * Enforces the fact that `xtor` has to fully match the `xtee`. + * (Enforced by the extraction algorithm but having a final check might be a good idea) + * For instance, this [[Strategy]] is necessary for extraction as + * the pattern `xtor` has to match the entire `xtee`. + */ + case object CompleteMatching extends Strategy + + + + + // * --- * --- * --- * Instructions * --- * --- * --- * - import scala.reflect.runtime.universe.TypeTag // TODO do without this + /** + * Specifies what the extraction should do. + */ + sealed trait Instruction + + /** + * Instructs the extraction has to look for a matching statements. + * This instruction is attached to impure statements as well as pure statements + * that are not used by other statements. Giving this instructions to those two cases + * means that the unused statements are handled the same way. So, even though they are pure, + * unused statements will be matched in order. + */ + case object Start extends Instruction + + /** + * Instructs the extraction can ignore this statement. + * This instruction is attached to all pure statements that are used by the return value. + */ + case object Skip extends Instruction - def uninterpretedType[A: TypeTag]: TypeRep = unsupported - def typeApp(self: TypeRep, typ: TypSymbol, targs: List[TypeRep]): TypeRep = unsupported - def staticTypeApp(typ: TypSymbol, targs: List[TypeRep]): TypeRep = DummyTypeRep //unsupported - def recordType(fields: List[(String, TypeRep)]): TypeRep = unsupported - def constType(value: Any, underlying: TypeRep): TypeRep = unsupported - def typLeq(a: TypeRep, b: TypeRep): Boolean = unsupported + case class Instructions(flags: Set[BoundVal]) { + def get(bv: BoundVal): Instruction = if (flags contains bv) Start else Skip + } - def loadTypSymbol(fullName: String): TypSymbol = new TypeSymbol // TODO + object Instructions { + def apply(xtor: Rep, xtee: Rep): Instructions = Instructions(gen(xtor) ++ gen(xtee)) + + /** + * Generates the instructions that will be flagged with [[Start]]. + * These will include the impure statements and the unused statements. + */ + def gen(r: Rep): Set[BoundVal] = { + def update(d: Def, unusedBVs: Set[BoundVal], impures: Set[BoundVal]): (Set[BoundVal], Set[BoundVal]) = d match { + case l: Lambda => genInstructions0(l.body, unusedBVs, impures) + + case ma: MethodApp => ((ma.self :: ma.argss.argssList).foldLeft(unusedBVs) { + case (acc, bv: BoundVal) => acc - bv + case (acc, _) => acc + }, impures) + + case _ => (unusedBVs, impures) + } + + /* + * The unused BVs are kept separate from the impures even thought both will be merged at the end in order to be + * able to keep track of the unused statements when recursing in `r`. + */ + def genInstructions0(r: Rep, unusedBVs: Set[BoundVal], impures: Set[BoundVal]): (Set[BoundVal], Set[BoundVal]) = r match { + case lb: LetBinding => + val updated = update( + lb.value, + unusedBVs + lb.bound, + effect(lb.value) match { + case Pure => impures + case Impure => impures + lb.bound + } + ) + genInstructions0(lb.body, updated._1, updated._2) + + case bv: BoundVal => (unusedBVs - bv, impures) + + case ByName(r) => genInstructions0(r, unusedBVs, impures) + + case _ => (unusedBVs, impures) + } + + val instructions = genInstructions0(r, Set.empty, Set.empty) + instructions._1 ++ instructions._2 + } + } + def extractWithState(xtor: Rep, xtee: Rep)(implicit es: State): ExtractState = { + def extractHOPHole(name: String, typ: TypeRep, argss: List[List[Rep]], visible: List[BoundVal])(implicit es: State): ExtractState = { + def hasNoUndeclaredUsages(r: Rep): Boolean = { + def hasNoUndeclaredUsages0(r: Rep, declared: Set[BoundVal]): Boolean = r match { + case bv: BoundVal => declared contains bv + case lb: LetBinding => + val declared0 = declared + lb.bound + defHasNoUndeclaredUsages(lb.value, declared0) && hasNoUndeclaredUsages0(lb.body, declared0) + case _ => true + } + + def defHasNoUndeclaredUsages(d: Def, declared: Set[BoundVal]): Boolean = d match { + case l: Lambda => hasNoUndeclaredUsages0(l.body, declared + l.bound) + case ma: MethodApp => (ma.self :: ma.argss.argssList) forall (hasNoUndeclaredUsages0(_, declared)) + case _ => true + } + + hasNoUndeclaredUsages0(r, Set.empty) + } + + /** + * Attemps to find the `xtors` in the body of the function and replaces them with newly generated arguments, + * adding the new arguments to the function. Even if the `xtors` are not found in the body, + * arguments representing them will be generated and added to it. They will simply not appear in the function's body. + */ + def buildFunc(xtors: List[Rep], maybeFuncAndState: Option[(Rep, State)]): Option[(Rep, State)] = { + val args = xtors.map(arg => bindVal("hopArg", arg.typ, Nil)) + val transformations = xtors zip args + + /** + * Returns the body of function. + * This is the body of the most deeply nested [[Lambda]] since the function is curried. + */ + def body(func: Rep): Rep = { + def body0(d: Def): Option[Rep] = d match { + case l: Lambda => l.body match { + case lb: LetBinding => Some(body(lb)) + case body => Some(body) + } + case _ => None + } + + func match { + case lb: LetBinding => body0(lb.value) getOrElse lb + case _ => func + } + } + + for { + (f, es) <- maybeFuncAndState + + newBodyAndState = transformations.foldLeft(body(f) -> es) { + case ((body, es), (xtor, arg)) => xtor match { + case bv: BoundVal => + /* + * Assumes the bv is already in the context. + * This is enforced by how the instructions are chosen. + */ + es.ctx(bv) rebind arg + body -> es + + case lb: LetBinding => + + /** + * Replaces all occurrences of `lb` in the body by `arg`. + */ + def replaceAllOccurrences(body: Rep)(es: State): Rep -> State = { + def replaceAllOccurrences0(body: Rep)(implicit es: State): Rep -> State = { + def filterLBs(r: Rep)(p: LetBinding => Boolean): Rep = r match { + case lb: LetBinding if p(lb) => filterLBs(lb.body)(p) + case lb: LetBinding => + lb.body = filterLBs(lb.body)(p) + lb + case _ => r + } + + /* + * Replaces every occurrence of `lb` in the body with `arg`. + */ + // TODO single pass + extractWithState(lb, body) map { es0 => + val replace = es0.ctx(lb.last.bound) + val body0 = bottomUpPartial(filterLBs(body)(es0.ctx.values.toSet contains _.bound)) { case `replace` => arg } + replaceAllOccurrences0(body0) + } getOrElse body -> es + } + + // We only want to extract `lb` and not rewrite it so the strategy is changed to `PartialMatching`. + replaceAllOccurrences0(body)(es withInstructionsFor lb withStrategy PartialMatching) + .mapSecond(_.withDefaultInstructions.withDefaultStrategy) // Reset the strategy so it resume doing a `CompleteMatching`. + } + + replaceAllOccurrences(body)(es) + + // TODO implement this when we allow holes in HOPHoles + // case Hole(n, t) => ??? + + case _ => bottomUpPartial(body) { case `xtor` => arg } -> es + } + } + + // The body of the extracted function should not contains any references to elements of `visible`. + _ = bottomUpPartial(newBodyAndState._1) { case bv: BoundVal if visible contains bv => return None } + } yield newBodyAndState match { + case (func0, es0) => wrapConstruct(lambda(args, func0)) -> es0 + } + } + + val maybeES = for { + es1 <- typ extract (xtee.typ, Covariant) + m <- merge(es.ex, es1) + + /** + * The arguments of HOPHoles are _always_ passed by-name + */ + argss0 = argss.map(_.map { + case ByName(r) => r + case _ => die // All HOPHole args have to be by-name! + }) + + (f, es2) <- argss0.foldRight(Option(xtee -> (es withNewExtract m)))(buildFunc) + // ^ so arguments are in the right order + + if hasNoUndeclaredUsages(f) + } yield es2 updateExtractWith Some(repExtract(name -> f)) + + maybeES getOrElse fail + } + + def extractLBs(lb1: LetBinding, lb2: LetBinding)(implicit es: State): ExtractState = { + def extractAndContinue(lb1: LetBinding, lb2: LetBinding)(implicit es: State): ExtractState = for { + es1 <- extractWithState(lb1.bound, lb2.bound) + es2 <- extractWithState(lb1.body, lb2.body)(es1) + } yield es2 + + (es.instructions.get(lb1.bound), es.instructions.get(lb2.bound)) match { + case (Start, Start) => extractAndContinue(lb1, lb2) + + /** + * In the following case `aX` has to be extracted since when extracting the + * HOPHole `h` the BV has to be matched before it (see reason in [[extractHOPHole]]. + * + * XTOR: ir"val aX = 10.toDouble; $h(aX)" + * \-------Start------/ + * XTEE: ir"val a = 10.toDouble; a + 1" + * \------Skip------/ + */ + case (Start, Skip) => for { + failed1 <- extractAndContinue(lb1, lb2).left + failed2 <- extractWithState(lb1, lb2.body)(es withFailed failed1).left + } yield failed1 ++ failed2 + + case (Skip, Start) => extractWithState(lb1.body, lb2) + + case (Skip, Skip) => extractWithState(lb1.body, lb2.body) + } + } + + def extractHole(h: Hole, r: Rep)(implicit es: State): ExtractState = h match { + case Hole(n, t) => + es.updateExtractWith( + t extract(xtee.typ, Covariant), + Some(repExtract(n -> r)) + ) map (_ withMatchedImpures r) + } + + /** + * Attemps to extract `r` by trying to match it with the component of `d`. + * Only extracts inside a [[MethodApp]], fails for all other cases. + * It won't extract inside the [[ByName]] arguments. + */ + def extractInside(r: Rep, d: Def)(implicit es: State): ExtractState = d match { + case ma: MethodApp => (ma.self :: ma.argss.argssList).foldLeft[ExtractState](fail) { case (acc, arg) => + for { + failed1 <- acc.left + failed2 <- extractWithState(r, arg)(es withFailed failed1).left + } yield failed1 ++ failed2 + } + case _ => fail + } + + + def extractedBy(h: Hole)(implicit es: State): Option[Rep] = es.ex._1 get h.name + + xtor -> xtee match { + case (h: Hole, _) => extractedBy(h) match { + case Some(`xtee`) => succeed + case Some(_) => die // Something has gone wrong + case None => extractHole(h, xtee) + } + + case (HOPHole2(name, typ, argss, visible), _) => extractHOPHole(name, typ, argss, visible) + + case (lb1: LetBinding, lb2: LetBinding) => extractLBs(lb1, lb2) + + case (lb: LetBinding, _) => es.instructions.get(lb.bound) match { + case Start => fail // The `xtor` has more impure statements than the `xtee`. + case Skip => extractWithState(lb.body, xtee) + } + + case (bv: BoundVal, lb: LetBinding) => + if (es.ctx.keySet contains bv) succeed // `bv` has already been extracted + else es.strategy match { + case CompleteMatching => es.instructions.get(lb.bound) match { + + // The `xtee` has more impure statements or dead-ends than the `xtor` + case Start => fail + + // The skipped statements of the `xtee` will be matched later + // when extracting its return value or the `Start` ones. + case Skip => extractWithState(bv, lb.body) + } + + // Attempts to extract the return value `bv` by trying + // 1. The current let-binding + // 2. The components of the let-bindings value + // 3. (1. and then 2.) on the next statement + case PartialMatching => for { + failed1 <- extractWithState(bv, lb.bound).left + failed2 <- extractInside(bv, lb.value)(es withFailed failed1).left + failed3 <- extractWithState(bv, lb.body)(es withFailed (failed1 ++ failed2)).left + } yield failed1 ++ failed2 ++ failed3 + } + + case (Constant(()), _: LetBinding) => es.strategy match { + // Unit return doesn't have to matched + case PartialMatching => succeed + case CompleteMatching => fail + } + + + case (_: Rep, lb: LetBinding) => es.strategy match { + case PartialMatching => for { + failed1 <- extractInside(xtor, lb.value).left + failed2 <- extractWithState(xtor, lb.body)(es withFailed failed1).left + } yield failed1 ++ failed2 + + case CompleteMatching => fail + } + + // Only a [[ByName]] can extract a [[ByName]] so that + // the rewriting will rewrite inside the [[ByName]]. + case (ByName(r1), ByName(r2)) => extractWithState(r1, r2) + + case (_, Ascribe(s, _)) => extractWithState(xtor, s) + + case (Ascribe(s, t), _) => for { + es1 <- es.updateExtractWith(t extract(xtee.typ, Covariant)) + es2 <- extractWithState(s, xtee)(es1) + } yield es2 + + case (HOPHole(name, typ, argss, visible), _) => extractHOPHole(name, typ, argss, visible) + + // The actual extraction happens here. + case (bv1: BoundVal, bv2: BoundVal) => es.ctx.get(bv1) map { extractedByBV1 => + if (extractedByBV1 == bv2) succeed + else fail + } getOrElse { + if (bv1 == bv2) succeed + else if (es.failed.getOrElse(bv1, Set.empty) contains bv2) fail // Previously failed to extract `bv1` with `bv2` + else (bv1.owner, bv2.owner) match { + case (lb1: LetBinding, lb2: LetBinding) => extractDefs(lb1.value, lb2.value) match { + case Right(es) => effect(lb2.value) match { + case Pure => succeed(es withCtx lb1.bound -> lb2.bound) + case Impure => succeed(es withCtx lb1.bound -> lb2.bound withMatchedImpure lb2.bound) + } + case Left(failed) => failWith(failed + (lb1.bound -> lb2.bound)) + } + case (l1: Lambda, l2: Lambda) => + // We cannot know the owner of the [[Lambda]] + // so in case of a failure to extract there's nothing to do. + extractDefs(l1, l2) map (_ withCtx l1.bound -> l2.bound) + case _ => failWith(Set(bv1 -> bv2)) + } + } + + case (Constant(v1), Constant(v2)) if v1 == v2 => es updateExtractWith (xtor.typ extract(xtee.typ, Covariant)) + + // Assuming if they have the same name the type is the same + case (StaticModule(fn1), StaticModule(fn2)) if fn1 == fn2 => succeed + + // Assuming if they have the same name and prefix the type is the same + case (Module(p1, n1, _), Module(p2, n2, _)) if n1 == n2 => extractWithState(p1, p2) + + case (NewObject(t1), NewObject(t2)) => es updateExtractWith (t1 extract(t2, Covariant)) + + case _ => fail + } + } + + protected def spliceExtract(xtor: Rep, args: Args): Option[Extract] = xtor match { + // Should check that type matches, but don't see how to access it for Args + case SplicedHole(n, _) => Some(Map(), Map(), Map(n -> args.reps)) + + case Hole(n, t) => + val rep = methodApp( + staticModule("scala.collection.Seq"), + loadMtdSymbol( + loadTypSymbol("scala.collection.generic.GenericCompanion"), + "apply", + None), + List(t), + List(Args()(args.reps: _*)), + staticTypeApp(loadTypSymbol("scala.collection.Seq"), List(t))) + Some(repExtract(n -> rep)) + + case _ => throw IRException(s"Trying to splice-extract with invalid extractor $xtor") + } + + def extractDefs(v1: Def, v2: Def)(implicit es: State): ExtractState = (v1, v2) match { + case (l1: Lambda, l2: Lambda) => for { + es1 <- es updateExtractWith (l1.boundType extract(l2.boundType, Covariant)) + es2 <- extractWithState(l1.body, l2.body)(es1 withCtx l1.bound -> l2.bound withStrategy CompleteMatching) + } yield es2 withDefaultStrategy + + case (ma1: MethodApp, ma2: MethodApp) if ma1.mtd == ma2.mtd => + def targExtract(es0: State): ExtractState = + es0.updateExtractWith((for { + (e1, e2) <- ma1.targs zip ma2.targs + } yield e1 extract(e2, Invariant)): _*) + + def extractArgss(argss1: ArgumentLists, argss2: ArgumentLists)(implicit es: State): ExtractState = (argss1, argss2) match { + case (ArgumentListCons(h1, t1), ArgumentListCons(h2, t2)) => for { + es0 <- extractArgss(h1, h2) + es1 <- extractArgss(t1, t2)(es0) + } yield es1 + + case (ArgumentCons(h1, t1), ArgumentCons(h2, t2)) => for { + es0 <- extractArgss(h1, h2) + es1 <- extractArgss(t1, t2)(es0) + } yield es1 + + case (SplicedArgument(arg1), SplicedArgument(arg2)) => extractWithState(arg1, arg2) + case (SplicedArgument(arg), ac: ArgumentCons) => es updateExtractWith spliceExtract(arg, Args(ac.argssList: _*)) + case (SplicedArgument(arg), r: Rep) => es updateExtractWith spliceExtract(arg, Args(r)) + case (SplicedArgument(_), NoArguments) => succeed + + case (NoArguments, NoArguments) => succeed + case (NoArgumentLists, NoArgumentLists) => succeed + + case (r1: Rep, r2: Rep) => extractWithState(r1, r2) + + case _ => fail + } + + for { + es1 <- extractWithState(ma1.self, ma2.self) + es2 <- targExtract(es1) + es3 <- extractArgss(ma1.argss, ma2.argss)(es2) + es4 <- es3.updateExtractWith(ma1.typ extract (ma2.typ, Covariant)) + } yield es4 + + // Assuming a [[DefHole]] only extracts impure statements + case (DefHole(h), _) if !isPure(v2) => extractWithState(h, wrapConstruct(letbind(v2))) + + case _ => fail + } + override def rewriteRep(xtor: Rep, xtee: Rep, code: Extract => Option[Rep]): Option[Rep] = + rewriteRep0(xtor, xtee, code)(State.forRewriting(xtor, xtee)) + + def rewriteRep0(xtor: Rep, xtee: Rep, code: Extract => Option[Rep])(implicit es: State): Option[Rep] = { + def rewriteRepWithState(xtor: Rep, xtee: Rep)(implicit es: State): ExtractState = (xtor, xtee) match { + case (lb1: LetBinding, lb2: LetBinding) => + + /** + * Pure statements (annotated with the instruction [[Skip]] only have to extracted starting from their + * return value and extract each sub-part recursively. Through this mechanism the order of the pure statements + * does not matter. + * For instance, this will successfully match : + * {{{ + * ir"val b = 22.toDouble; val a = 11.toDouble; a + b" match { + * case ir"val aX = 11.toDouble; val bX = 22.toDouble; aX + bX" => ??? + * } + * }}} + * + */ + (es.instructions.get(lb1.bound), es.instructions.get(lb2.bound)) match { + + /** + * The traversal of the code is done externally by [[transformRep()]]. + * Hence, if the current statements don't have to be extracted at this point (both are pure and not return values) + * we simply skip the extraction of the current `xtee`. + */ + case (Skip, Skip) => fail + + case _ => extractWithState(lb1, lb2) + } + + case _ => extractWithState(xtor, xtee) + } + + def genCode(implicit es: State): Option[Rep] = { + + /** + * Returns true if all the BV usages appearing in the extraction are declared inside the extraction and, if not, + * that it has been declared by the user. + * For instance: + * {{{ + * ir"val r = readInt; r + 1" rewrite { + * case ir"val rX = readInt; $body" => ??? + * } + * }}} + * `$body` will extract `ir"r + 1"` where `r` <-> `rX`. Since the let-binding `val rX = readInt; ...` + * is user-defined. + */ + def preCheck(ex: Extract): Boolean = { + def preCheckRep(declaredBVs: Set[BoundVal], invCtx: Map[BoundVal, Set[BoundVal]], r: Rep): Boolean = { + def preCheckDef(declaredBVs: Set[BoundVal], invCtx: Map[BoundVal, Set[BoundVal]], d: Def): Boolean = { + d match { + case l: Lambda => preCheckRep(declaredBVs, invCtx, l.body) + case ma: MethodApp => (ma.self :: ma.argss.argssList) forall { + case bv: BoundVal => + (declaredBVs contains bv) || + ((for { + bvs <- invCtx.get(bv) + isUserDefined = bvs map (_.owner) forall { + case lb: LetBinding => lb.isUserDefined + case _ => true + } + } yield isUserDefined) getOrElse false) + case lb: LetBinding => preCheckRep(declaredBVs, invCtx, lb) + case _ => true + } + case _ => true + } + } + + r match { + case lb: LetBinding => + val acc0 = declaredBVs + lb.bound + preCheckDef(acc0, invCtx, lb.value) && preCheckRep(acc0, invCtx, lb.body) + case _ => true + } + } + + def reverse[A, B](m: Map[A, B]): Map[B, Set[A]] = m.groupBy(_._2).mapValues(_.keys.toSet) + + val invCtx = reverse(es.ctx) + ex._1.values ++ ex._3.values.flatten forall (preCheckRep(Set.empty, invCtx, _)) + } + + def collectBVs(d: Def): Set[BoundVal] = d match { + case ma: MethodApp => ma.self :: ma.argss.argssList collect { case bv: BoundVal => bv } toSet + case _ => Set.empty + } + + /** + * Removes all the statements referencing elements from `remove`. + * Also takes care of statements referencing removed statements. + * + * Returns None, if `r`'s return value is also removed. + * For instance in {{{filterNot(ir"val a = readInt; a", Set(a)}}} + */ + def filterNot(remove: Set[BoundVal])(r: Rep): Option[Rep] = { + + /** + * Returns the statements to remove based on the initial `remove` set. + * + * Adds all the pure statements referencing removed ones. + * In essence, computes the the transitive closures of pure statements that depend on the BVs in `remove` + * and adds it to `remove`. + */ + def buildToRemove(r: Rep, remove: Set[BoundVal]): Set[BoundVal] = r match { + case lb: LetBinding if remove contains lb.bound => buildToRemove(lb.body, remove) + + case lb: LetBinding if isPure(lb.value) => + val bvsInValue = collectBVs(lb.value) + + if (bvsInValue exists (remove contains)) { + buildToRemove(lb.body, remove + lb.bound) + } else { + buildToRemove(lb.body, remove) + } + + case lb: LetBinding => buildToRemove(lb.body, remove) + + case _ => remove + } + + def filterNot0(remove: Set[BoundVal])(r: Rep): Rep = r match { + case lb: LetBinding if remove contains lb.bound => filterNot0(remove)(lb.body) + case lb: LetBinding => + lb.body = filterNot0(remove)(lb.body) + lb + case _ => r + } + + val remove0 = buildToRemove(r, remove) + r match { + case lb: LetBinding => lb.last.body match { + case bv: BoundVal if remove0 contains bv => None + case _ => Some(filterNot0(remove0)(r)) + } + case _ => Some(r) // Nothing to do + } + } + + + /** + * Merges the generated `code` with the `xtee` + */ + def merge(code: Rep, xtee: Rep)(xtor: Rep, ctx: Ctx): Rep = { + + /** + * Puts `code` in the right position in `xtee`. + */ + def mergeLBs(code: LetBinding, xtee: LetBinding)(es: State): Rep = { + def collectAllBVs(r: Rep): Set[BoundVal] = { + def collectAllBVs0(r: Rep, acc: Set[BoundVal]): Set[BoundVal] = r match { + case lb: LetBinding => collectAllBVs0(lb.body, acc ++ collectBVs(lb.value)) + case _ => acc + } + collectAllBVs0(r, Set.empty) + } + + /** + * Returns the statement in `r` at which point all the BVs from `lookFor` have been declared, + * if it exists. + */ + def findPos(r: LetBinding, lookFor: Set[BoundVal]): Option[LetBinding] = { + if (lookFor.isEmpty) None + else r match { + case lb: LetBinding => + val lookFor0 = lookFor -- collectBVs(lb.value) + if (lookFor0.isEmpty) Some(lb) + else lb.body match { + case innerLB: LetBinding => findPos(innerLB, lookFor0) + case _ => None + } + case _ => None + } + } + + // All usages of BVs that come from the xtee + val lookFor = collectAllBVs(code).filter((es.ex._1.values ++ es.ex._3.values.flatten).toSet contains) + + findPos(xtee, lookFor) match { + case Some(pos) => + code.last.body = pos.body + pos.body = code + xtee + case None => + code.last.body = xtee + code + } + } + + xtor match { + // Find what the return value of `xtor` matched in `xtee` and + // replace that by the return value of `code`. + case xtorLB: LetBinding => + val xtorLast = xtorLB.last + xtorLast.body match { + case xtorRet: BoundVal => code match { + case codeLB: LetBinding => + val codeLast = codeLB.last + val codeRet = codeLast.body + val bv = ctx(xtorRet) + bottomUpPartial(xtee) { case `bv` => codeRet } match { + case xteeLB: LetBinding => mergeLBs(codeLB, xteeLB)(es) + case r => + codeLast.body = r + code + } + + case _ => + val bv = ctx(xtorRet) + bottomUpPartial(xtee) { case `bv` => code } + } + case _ => code + } + + case _ => code match { + case codeLB: LetBinding => + val codeLast = codeLB.last + val codeRet = codeLast.body + bottomUpPartial(xtee) { case `xtor` => codeRet } match { + case xteeLB: LetBinding => mergeLBs(codeLB, xteeLB)(es) + case r => + codeLast.body = r + code + } + + case _ => bottomUpPartial(xtee) { case `xtor` => code } + } + } + } + + if (preCheck(es.ex)) for { + code <- code(es.ex) + mergedCode = merge(code, xtee)(xtor, es.ctx) + finalCode <- filterNot(es.matchedImpureBVs)(mergedCode) + } yield finalCode + else None + } + + rewriteRepWithState(xtor, xtee) match { + case Right(es) => genCode(es) + case Left(_) => None + } + } + + // * --- * --- * --- * Implementations of `QuasiBase` methods * --- * --- * --- * + + def hole(name: String, typ: TypeRep) = Hole(name, typ) + def splicedHole(name: String, typ: TypeRep): Rep = SplicedHole(name, typ) + def typeHole(name: String): TypeRep = DummyTypeRep + def hopHole(name: String, typ: TypeRep, yes: List[List[BoundVal]], no: List[BoundVal]) = HOPHole(name, typ, yes, no) + override def hopHole2(name: String, typ: TypeRep, args: List[List[Rep]], visible: List[BoundVal]) = + HOPHole2(name, typ, args, visible filterNot (args.flatten contains _)) + def substitute(r: => Rep, defs: Map[String, Rep]): Rep = { + val r0 = + if (defs isEmpty) r + else bottomUp(r) { + case h@Hole(n, _) => defs getOrElse(n, h) + case h@SplicedHole(n, _) => defs getOrElse(n, h) + case h => h + } + + r0 |> inlineBlock + } + override def insertAfterTransformation(r: => Rep, defs: Map[String, Rep]): Rep = { + // TODO for now we do nothing to r. Later make sure that after applying the defs it is still valid in ANF! + require(defs.isEmpty) + r + } + + + // * --- * --- * --- * Implementations of `TypingBase` methods * --- * --- * --- * + + import scala.reflect.runtime.universe.TypeTag // TODO do without this + + def uninterpretedType[A: TypeTag]: TypeRep = DummyTypeRep + def typeApp(self: TypeRep, typ: TypSymbol, targs: List[TypeRep]): TypeRep = DummyTypeRep + def staticTypeApp(typ: TypSymbol, targs: List[TypeRep]): TypeRep = DummyTypeRep //unsupported + def recordType(fields: List[(String, TypeRep)]): TypeRep = DummyTypeRep + def constType(value: Any, underlying: TypeRep): TypeRep = DummyTypeRep + + def typLeq(a: TypeRep, b: TypeRep): Boolean = true + + def loadTypSymbol(fullName: String): TypSymbol = new TypeSymbol(fullName) // TODO + + // * --- * --- * --- * Misc * --- * --- * --- * def unsupported = lastWords("This part of the IR is not yet implemented/supported") @@ -143,6 +1164,10 @@ class FastANF extends InspectableBase with CurryEncoding with ScalaCore { class ReificationContext(val inExtractor: Bool) { reif => var firstLet: FlatOpt[LetBinding] = Non var curLet: FlatOpt[LetBinding] = Non + + /** + * Updates the current let-binding with `lb`. + */ def += (lb: LetBinding): Unit = { curLet match { case Non => firstLet = lb.som @@ -150,10 +1175,15 @@ class ReificationContext(val inExtractor: Bool) { reif => } curLet = lb.som } + + /** + * Let-binds `d` and updates the current let-binding with it. + */ def += (d: Def): Symbol = new Symbol { protected var _parent: SymbolParent = new LetBinding("tmp", this, d, this) alsoApply (reif += _) } - def finalize(r: Rep) = { + + def finalize(r: Rep): Rep = { firstLet match { case Non => assert(curLet.isEmpty) diff --git a/src/main/scala/squid/ir/fastanf/Rep.scala b/src/main/scala/squid/ir/fastanf/Rep.scala index 4606314c..64ce9f23 100644 --- a/src/main/scala/squid/ir/fastanf/Rep.scala +++ b/src/main/scala/squid/ir/fastanf/Rep.scala @@ -37,25 +37,50 @@ object Som { /** Trivial expression, that can be used as arguments */ sealed abstract class Def extends DefOption with DefOrTypeRep with FlatSom[Def] { def fold[R](df: Def => R, typeRep: TypeRep => R): R = df(this) + def map(f: Def => Def): Def = f(this) } /** Expression that can be used as an argument or result; this includes let bindings. */ sealed abstract class Rep extends RepOption with ArgumentList with FlatSom[Rep] { def typ: TypeRep def * = SplicedArgument(this) + def argssMap(f: Rep => Rep) = f(this) + def argssList: List[Rep] = this :: Nil } +// TODO check that it only contains args +/** + * Wraps _arguments_ to make them by-name arguments. + */ +final case class ByName(r: Rep) extends Rep { + val typ: TypeRep = r.typ +} + final case class Constant(value: Any) extends Rep with CachedHashCode { - lazy val typ = value match { // TODO impl and rm lazy - case _ => lastWords(s"Not a valid constant: $value") + // TODO impl and rm lazy + lazy val typ = value match { + case _ => DummyTypeRep } } +final case class Hole(name: String, typ: TypeRep) extends Rep with CachedHashCode + +final case class SplicedHole(name: String, typ: TypeRep) extends Rep with CachedHashCode + +final case class HOPHole(name: String, typ: TypeRep, args: List[List[Symbol]], visible: List[Symbol]) extends Rep with CachedHashCode + +final case class HOPHole2(name: String, typ: TypeRep, args: List[List[Rep]], visible: List[Symbol]) extends Rep with CachedHashCode + // TODO intern objects final case class StaticModule(fullName: String) extends Rep with CachedHashCode { - lazy val typ = ??? + val typ = DummyTypeRep + override def toString = fullName } +final case class Module(prefix: Rep, name: String, typ: TypeRep) extends Rep with CachedHashCode + +final case class NewObject(typ: TypeRep) extends Rep with CachedHashCode + // TODO make sure this does not generate a field for `base` // if it does, perhaps use `private[this] implicit val`? final case class Ascribe(self: Rep, typ: TypeRep)(implicit base: FastANF) extends Rep with CachedHashCode { @@ -70,6 +95,9 @@ trait MethodApp extends Def { def targs: List[TypeRep] def argss: ArgumentLists def typ: TypeRep + protected def doChecks = // we can't execute the checks here right away, because of initialization order + //(self :: argss.argssList).foreach(r => assert(!r.isInstanceOf[LetBinding], s"Illegal ANF argument/self: $r")) + assert(!self.isInstanceOf[LetBinding], s"Illegal ANF self argument: $self") // ^ some arguments may be by-name! override def toString = s"$self.${mtd.name}${argss.toArgssString}" } object MethodApp { @@ -81,9 +109,93 @@ object MethodApp { case _ => SimpleMethodApp(self: Rep, mtd: MethodSymbol, targs: List[TypeRep], argss)(typ) } } + + /** + * Inlines `self` and the arguments `argss` if they are let-bindings in order to transform it into valid ANF. + */ + def toANF(self: Rep, mtd: MethodSymbol, targs: List[TypeRep], argss: ArgumentLists, typ0: TypeRep)(implicit base: FastANF): Rep = { + def processArgss(argss: ArgumentLists)(f: Rep => (Option[LetBinding], Rep)): (Option[LetBinding], ArgumentLists) = { + def processArgs(args: ArgumentList)(f: Rep => (Option[LetBinding], Rep)): (Option[LetBinding], ArgumentList) = { + def go(args: ArgumentList)(k: (Option[LetBinding], ArgumentList) => (Option[LetBinding], ArgumentList)): (Option[LetBinding], ArgumentList) = args match { + case NoArguments => k(None, NoArguments) + case ArgumentCons(h, t) => + val (maybeLB, r) = f(h) + go(t) { case (lb0, args0) => + val maybeNewLB = (lb0, maybeLB) match { + case (Some(lb0), Some(lb)) => lb.last.body = lb0; Some(lb) + case (_, Some(lb)) => Some(lb) + case (Some(lb0), _) => Some(lb0) + case _ => None + } + + k(maybeNewLB, ArgumentCons(r, args0)) + } + case SplicedArgument(arg) => k.tupled(f(arg)) + case r: Rep => k.tupled(f(r)) + } + + go(args)((lb, args) => (lb, args)) + } + + def go(argss: ArgumentLists)(k: (Option[LetBinding], ArgumentLists) => (Option[LetBinding], ArgumentLists)): (Option[LetBinding], ArgumentLists) = argss match { + case NoArgumentLists => k(None, NoArgumentLists) + case NoArguments => k(None, NoArguments) + case a: ArgumentCons => processArgs(a)(f) + case s: SplicedArgument => processArgs(s)(f) + case ArgumentListCons(h, t) => + val (lb, args) = processArgs(h)(f) + go(t) { case (lb0, argss0) => + val newLB = (lb0, lb) match { + case (Some(lb0), Some(lb)) => lb.last.body = lb0; Some(lb) + case (_, Some(lb)) => Some(lb) + case (Some(lb0), _) => Some(lb0) + case _ => None + } + k(newLB, ArgumentListCons(args, argss0)) + } + case r: Rep => k.tupled(f(r)) + } + + go(argss)((lb, argss) => (lb, argss)) + } + + /** + * Splits `r` at before the point of its return value + */ + def split(r: Rep): Option[LetBinding] -> Rep = r match { + case lb: LetBinding => Some(lb) -> lb.last.body + case _ => None -> r + } + + val (maybeSelfLB, selfBV) = self |> split + val (maybeArgssLB, argssBVs) = processArgss(argss)(split) + + // Merge `maybeSelfLB` and `maybeArgssLB` + val maybeNewLB = (maybeSelfLB, maybeArgssLB) match { + case (Some(selfLB), Some(lbFromArgss)) => lbFromArgss.last.body = selfLB; Some(lbFromArgss) + case (_, Some(lbFromArgss)) => Some(lbFromArgss) + case (Some(selfLB), _) => Some(selfLB) + case _ => None + } + + val maWithBVs = MethodApp(selfBV, mtd, targs, argssBVs, typ0) + + // Set `maWithBVs` as the body of `maybeNewLB` + maybeNewLB match { + case Some(newLB) => + newLB.last.body = new Symbol { + protected var _parent: SymbolParent = new LetBinding("tmp", this, maWithBVs, this) + }.owner.asInstanceOf[LetBinding] + newLB + + case None => new Symbol { + protected var _parent: SymbolParent = new LetBinding("tmp", this, maWithBVs, this) + }.owner.asInstanceOf[LetBinding] + } + } } final case class SimpleMethodApp protected(self: Rep, mtd: MethodSymbol, targs: List[TypeRep], argss: ArgumentLists)(val typ: TypeRep) - extends MethodApp with CachedHashCode + extends MethodApp with CachedHashCode { doChecks } final case class App(fun: Rep, arg: Rep)(implicit base: FastANF) extends Def with MethodApp { val self = fun @@ -91,6 +203,11 @@ final case class App(fun: Rep, arg: Rep)(implicit base: FastANF) extends Def wit def targs = Nil def argss = arg lazy val typ = fun.typ.asFunType.map(_._2).getOrElse(lastWords(s"Application on a non-function type `${fun.typ}`")) + doChecks +} + +final case class DefHole(hole: Hole) extends Def { + def typ: TypeRep = hole.typ } /** To avoid useless wrappers/boxing in the most common cases, we have this scheme for argument lists: @@ -112,43 +229,103 @@ sealed trait ArgumentLists extends CachedHashCode { def asSingleArg: Rep = this.asInstanceOf[Rep] def ~~: (as: ArgumentList): ArgumentLists = ArgumentListCons(as, this) - - def toArgssString = this match { - case NoArgumentLists => "" - case NoArguments => s"()" - case r: Rep => s"($r)" - case _ => ??? // TODO + + def toArgssString: String = { + def withoutParen(args: ArgumentList): String = args match { + case NoArguments => "" + case r: Rep => s"$r" + case SplicedArgument(r) => s"$r: _*" + case ArgumentCons(h, t: Rep) => s"$h, ${withoutParen(t)}" + case ArgumentCons(h, NoArguments) => s"$h" + case ArgumentCons(h, t) => s"$h, ${withoutParen(t)}" + } + + this match { + case NoArgumentLists => "" + case NoArguments => s"()" + case r: Rep => s"(${withoutParen(r)})" + case s: SplicedArgument => s"(${withoutParen(s)})" + case args: ArgumentCons => s"(${withoutParen(args)})" + case ArgumentListCons(h, t) => s"(${withoutParen(h)})${t.toArgssString}" + } } + + def argssMap(f: Rep => Rep): ArgumentLists + def argssList: List[Rep] // TODO use more efficient structure to accumulate args (with constant-time ++ and +:); also, make these lazy vals in non-trivial argument lists? } + final case object NoArgumentLists extends ArgumentLists { override def ~~: (as: ArgumentList): ArgumentLists = as + def argssMap(f: Rep => Rep) = this + def argssList: List[Rep] = Nil } sealed trait ArgumentList extends ArgumentLists { def ~: (a: Rep): ArgumentList = ArgumentCons(a, this) + def argssMap(f: Rep => Rep): ArgumentList } final case object NoArguments extends ArgumentList { override def ~: (a: Rep): ArgumentList = a + def argssMap(f: Rep => Rep) = this + def argssList: List[Rep] = Nil +} +// Q: can make extend AnyVal? requires making all upper traits universal) +final case class SplicedArgument(arg: Rep) extends ArgumentList { + def argssMap(f: Rep => Rep) = SplicedArgument(f(arg)) + def argssList: List[Rep] = arg :: Nil +} +final case class ArgumentCons(head: Rep, tail: ArgumentList) extends ArgumentList { + def argssMap(f: Rep => Rep) = ArgumentCons(f(head), tail argssMap f) + def argssList: List[Rep] = head :: tail.argssList +} +final case class ArgumentListCons(head: ArgumentList, tail: ArgumentLists) extends ArgumentLists { + def argssMap(f: Rep => Rep) = ArgumentListCons(head argssMap f, tail argssMap f) + def argssList: List[Rep] = head.argssList ++ tail.argssList } -final case class SplicedArgument(arg: Rep) extends ArgumentList // Q: can make extend AnyVal? requires making all upper traits universal) -final case class ArgumentCons(head: Rep, tail: ArgumentLists) extends ArgumentList -final case class ArgumentListCons(head: ArgumentList, tail: ArgumentLists) extends ArgumentLists trait Binding extends SymbolParent { - val name: String + def name: String def bound: Symbol def boundType: TypeRep } -class LetBinding(val name: String, val bound: Symbol, val value: Def, private var _body: Rep) extends Rep with Binding { +trait RebindableBinding extends Binding { + def bound_= (newBound: Symbol): Unit + def name_= (newName: String): Unit +} +class LetBinding(var name: String, var bound: Symbol, var value: Def, private var _body: Rep) extends Rep with RebindableBinding { + var isUserDefined = false def body = _body def body_= (newBody: Rep) = _body = newBody def boundType = value.typ def typ = body.typ + /** Returns the last let-bindings of this conceptual block of code. + * Caution: this has linear complexity */ + def last: LetBinding = body match { + case lb: LetBinding => lb.last + case _ => this + } override def toString: String = s"val $bound = $value; $body" } -class Lambda(val name: String, val bound: Symbol, val boundType: TypeRep, val body: Rep)(implicit base: FastANF) extends Def with Binding { +object LetBinding { + /** + * Inlines the `value` to transform it into valid ANF. + */ + def withRepValue(name: String, bound: Symbol, value: Rep, mkBody: => Rep): Rep = value match { + case lb: LetBinding => + val last = lb.last + last.name = name + bound rebind last.bound + last.bound = bound + last.body = mkBody + lb + + case _ => throw new IllegalArgumentException + } +} + +class Lambda(var name: String, var bound: Symbol, val boundType: TypeRep, var body: Rep)(implicit base: FastANF) extends Def with RebindableBinding { val typ: TypeRep = base.funType(boundType, body.typ) - override def toString: String = s"($bound: $boundType) => $body" + override def toString: String = s"($bound: $boundType) => $body --" } /** Currently used mainly for reification. */ // Note: could intern these objects @@ -171,13 +348,18 @@ abstract class Symbol extends Rep with SymbolParent { def representative: Symbol = owner.bound def owner: Binding = _parent match { case bnd: Binding => - assert(bnd.bound eq this) + //assert(bnd.bound eq this) // FIXME? still seems to crash; is the assertion correct? bnd case parent: Symbol => val bnd = parent.owner _parent = bnd bnd } + def owner_=(bnd: RebindableBinding) = { + // TODO add appropriate assertions...? + bnd.bound = this + _parent = bnd + } def typ = owner.boundType def dfn: DefOption = owner match { case bnd: LetBinding => bnd.value @@ -189,6 +371,11 @@ abstract class Symbol extends Rep with SymbolParent { case s: Symbol => s.representative eq representative case _ => false } - override def toString: String = s"${owner.name}#${System.identityHashCode(representative)}" + //override def toString: String = s"${owner.name}#${System.identityHashCode(representative)}" + /* below is for easier debugging -- revert to the above for better perf */ + val id = Symbol.curId alsoDo (Symbol.curId += 1); override def toString: String = s"${owner.name}_${representative.id}" +} +object Symbol { + private var curId = 0 } diff --git a/src/main/scala/squid/ir/fastanf/Symbols.scala b/src/main/scala/squid/ir/fastanf/Symbols.scala index fd2c8b58..9f4a5426 100644 --- a/src/main/scala/squid/ir/fastanf/Symbols.scala +++ b/src/main/scala/squid/ir/fastanf/Symbols.scala @@ -3,10 +3,10 @@ package ir.fastanf import utils._ -class TypeSymbol { +case class TypeSymbol(name: String) { } -case class MethodSymbol(name: String) { +case class MethodSymbol(typ: TypeSymbol, name: String) { } diff --git a/src/main/scala/squid/ir/fastanf/TypeRep.scala b/src/main/scala/squid/ir/fastanf/TypeRep.scala index 225e6f68..5ed853d3 100644 --- a/src/main/scala/squid/ir/fastanf/TypeRep.scala +++ b/src/main/scala/squid/ir/fastanf/TypeRep.scala @@ -4,7 +4,7 @@ package ir.fastanf import utils._ // cannot be sealed unless we put everything into one file... -private[fastanf] trait DefOrTypeRep { +/*private[fastanf]*/ trait DefOrTypeRep { def typ: TypeRep def fold[R](df: Def => R, typeRep: TypeRep => R): R } diff --git a/src/test/scala/squid/feature/HigherOrderPatternVariables.scala b/src/test/scala/squid/feature/HigherOrderPatternVariables.scala new file mode 100644 index 00000000..980e93f7 --- /dev/null +++ b/src/test/scala/squid/feature/HigherOrderPatternVariables.scala @@ -0,0 +1,59 @@ +package squid +package feature + +import utils._ + +class HigherOrderPatternVariables extends MyFunSuite { + import TestDSL.Predef._ + + test("Matching lambda bodies") { + + val id = ir"(z:Int) => z" + + ir"(a: Int) => a + 1" matches { + case ir"(x: Int) => $body(x):Int" => + body eqt ir"(_:Int)+1" + } and { + case ir"(x: Int) => $body(x):$t" => + body eqt ir"(_:Int)+1" + eqt(t, irTypeOf[Int]) + } and { + case ir"(x: Int) => ($exp(x):Int)+1" => + exp eqt id + } + + ir"(a: Int, b: Int) => a + 1" matches { + case ir"(x: Int, y: Int) => $body(y):Int" => fail + case ir"(x: Int, y: Int) => $body(x):Int" => + } and { + case ir"(x: Int, y: Int) => $body(x,y):Int" => + } + + ir"(a: Int, b: Int) => a + b" matches { + case ir"(x: Int, y: Int) => $body(x):Int" => fail + case ir"(x: Int, y: Int) => $body(x,y):Int" => + body eqt ir"(_:Int)+(_:Int)" + } and { + case ir"(x: Int, y: Int) => ($lhs(y):Int)+($rhs(y):Int)" => fail + case ir"(x: Int, y: Int) => ($lhs(x):Int)+($rhs(y):Int)" => + lhs eqt id + rhs eqt id + } + + } + + test("Matching let-binding bodies") { + + ir"val a = 0; val b = 1; a + b" matches { + case ir"val x: Int = $v; $body(x):Int" => + v eqt ir"0" + body matches { + case ir"(y:Int) => { val x: Int = $w; $body(x,y):Int }" => + w eqt ir"1" + body eqt ir"(u:Int,v:Int) => (v:Int)+(u:Int)" + } + } + + } + +} diff --git a/src/test/scala/squid/ir/fastir/ArgumentTests.scala b/src/test/scala/squid/ir/fastir/ArgumentTests.scala new file mode 100644 index 00000000..d16b9e12 --- /dev/null +++ b/src/test/scala/squid/ir/fastir/ArgumentTests.scala @@ -0,0 +1,36 @@ +package squid +package ir.fastir + +import squid.ir.FastANF + +class ArgumentTests extends MyFunSuiteBase(ArgumentTests.Embedding) { + test("Arguments") { + import squid.ir.fastanf._ + + val c0 = Constant(0) + val c1 = Constant(1) + val c2 = Constant(2) + + assert(c1 ~: c2 ~: NoArguments == ArgumentCons(c1, c2)) + assert(c0 ~~: (c1 ~: c2) ~~: NoArguments == ArgumentListCons(c0, ArgumentListCons(ArgumentCons(c1, c2), NoArguments))) + assert(c0 ~~: (c1 ~: c2 ~: NoArguments) ~~: NoArgumentLists == ArgumentListCons(c0, ArgumentCons(c1, c2))) + } + + test("ArgumentLists Pretty Print") { + import squid.ir.fastanf._ + + val c0 = Constant(0) + val c1 = Constant(1) + val c2 = Constant(2) + val c3 = Constant(3) + + assert((c0 ~: c1).toArgssString == s"($c0, $c1)") + assert((c0 ~~: (c1 ~: c2)).toArgssString == s"($c0)($c1, $c2)") + assert((c0 ~~: (c1 ~: c2 ~: NoArguments)).toArgssString == s"($c0)($c1, $c2)") + assert((c0 ~~: (c1 ~: c2 ~: SplicedArgument(c3))).toArgssString == s"($c0)($c1, $c2, $c3: _*)") + } +} + +object ArgumentTests { + object Embedding extends FastANF +} diff --git a/src/test/scala/squid/ir/fastir/BasicTests.scala b/src/test/scala/squid/ir/fastir/BasicTests.scala deleted file mode 100644 index 9dcbe68d..00000000 --- a/src/test/scala/squid/ir/fastir/BasicTests.scala +++ /dev/null @@ -1,34 +0,0 @@ -package squid -package ir.fastir - -import utils._ -import squid.ir.FastANF - -class BasicTests extends MyFunSuiteBase(BasicTests.Embedding) { - import BasicTests.Embedding.Predef._ - - // TODO make proper tests... - - test("Basics") { - - println(ir"(x:Int) => x.toDouble.toInt + 42.0.toInt") - - } - - test("Arguments") { - import squid.ir.fastanf._ - - val c0 = Constant(0) - val c1 = Constant(1) - val c2 = Constant(2) - - assert(c1 ~: c2 ~: NoArguments == ArgumentCons(c1, c2)) - assert(c0 ~~: (c1 ~: c2) ~~: NoArguments == ArgumentListCons(c0, ArgumentListCons(ArgumentCons(c1, c2), NoArguments))) - assert(c0 ~~: (c1 ~: c2 ~: NoArguments) ~~: NoArgumentLists == ArgumentListCons(c0, ArgumentCons(c1, c2))) - - } - -} -object BasicTests { - object Embedding extends FastANF -} diff --git a/src/test/scala/squid/ir/fastir/ExtractingTests.scala b/src/test/scala/squid/ir/fastir/ExtractingTests.scala new file mode 100644 index 00000000..f600f0b2 --- /dev/null +++ b/src/test/scala/squid/ir/fastir/ExtractingTests.scala @@ -0,0 +1,145 @@ +package squid +package ir +package fastir + +import scala.util.Try + +class ExtractingTests extends MyFunSuiteBase(ExtractingTests.Embedding) { + import ExtractingTests.Embedding.Predef._ + + test("Matching with pure statements") { + ir"42" match { + case ir"$h" => assert(h =~= ir"42") + } + + ir"42.toDouble" match { + case ir"($h: Int).toDouble" => assert(h =~= ir"42") + } + + ir"println(42.toDouble)" match { + case ir"println(($h: Int).toDouble)" => assert(h =~= ir"42") + } + + ir"(42, 1337)" match { + case ir"(($l: Int), ($r: Int))" => + assert(l =~= ir"42") + assert(r =~= ir"1337") + } + + ir"val a = 10.toDouble; val b = 42.toDouble; a + b" match { + case ir"($body: Double)" => assert(body =~= ir"val a = 10.toDouble; val b = 42.toDouble; a + b") + } + } + + test("Matching with impure statements") { + ir"val r = readInt; r + 1" match { + case ir"val r = ($h: Int); r + 1" => assert(h =~= ir"readInt") + } + + assert(Try { + ir"val r = 10.toDouble; r + 1" match { + case ir"val rX = 42.toDouble; $body" => fail + } + }.isFailure) + } + + test("Matching with dead-ends") { + ir"val a = 42.toDouble; val b = a + 1; 1337" match { + case ir"val aX = ($h1: Int).toDouble; val bX = aX + ($h2: Int); ($h3: Int)" => + assert(h1 =~= ir"42") + assert(h2 =~= ir"1") + assert(h3 =~= ir"1337") + } + + ir"val a = 10.toDouble; val b = 20.toDouble; val c = 30.toDouble; a + b" match { + case ir"val aX = ($a: Int).toDouble; val bX= 20.toDouble; val cX = ($c: Int).toDouble; aX + bX" => + assert(a =~= ir"10") + assert(c =~= ir"30") + } + + ir"val a = 10.toDouble; val b = 20.toDouble; val c = 30.toDouble; a + b" match { + case ir"val bX= 20.toDouble; val aX = ($a: Int).toDouble; val cX = ($c: Int).toDouble; aX + bX" => + assert(a =~= ir"10") + assert(c =~= ir"30") + } + + // TODO better dead-end handling, should try to match dead-ends only with dead-ends? + //ir"val a = 10.toDouble; val b = 20.toDouble; val c = 30.toDouble; a + b" match { + // case ir"val cX = ($c: Int).toDouble; val bX= 20.toDouble; val aX = ($a: Int).toDouble; aX + bX" => + // assert(a =~= ir"10") + // assert(c =~= ir"30") + //} + } + + test("Extracting impure statements") { + ir"val a = readInt; val b = readInt; a + b" match { + case ir"val aX = readInt; val bX = readInt; aX + bX" => + } + + assert(Try { + ir"val a = readInt; val b = readInt; a + b" match { + case ir"val aX = readInt; val bX = readInt; bX + aX" => fail + } + }.isFailure) + + assert(Try { + ir"(readInt, readDouble)" match { + case ir"(readDouble, readInt)" => fail + } + }.isFailure) + + ir"readInt; readDouble" match { + case ir"$a; $b" => + assert(a =~= ir"readInt") + assert(b =~= ir"readDouble") + } + + ir"val r1 = readInt; val a = 20 + r1; val r2 = readDouble; a + r2" match { + case ir"val r1 = ($r1: Int); val r2 = ($r2: Double); val a = 20 + r1; a + r2" => + assert(r1 =~= ir"readInt") + assert(r2 =~= ir"readDouble") + } + + ir"val r1 = readInt; val a = 20 + r1; val b = r1 * 2; val r2 = a + readDouble; r2 + b" match { + case ir"val r1 = readInt; val b = r1 * 2; val a = 20 + r1; val r2 = a + readDouble; r2 + b" => + } + + ir"val a = readInt; val b = readInt; b" match { + case ir"val t = ($h: Int); val bX = readInt; bX" => assert(h =~= ir"readInt") + } + } + + test("Extracting should not match a return in the middle of block") { + assert(Try { + ir"val a = readInt; a + 11 + 22" match { + case ir"val a = readInt; a + 11" => fail + } + }.isFailure) + } + + test("Squid paper") { + + // 3.2 + ir"${ir"2"} + 1" match { + case ir"($n: Int) + 1" => + val res = ir"$n - 1" + assert(res =~= ir"${ir"2"} - 1") + } + + // 3.6 + assert(Try { + ir"(x: Int) => x + 1" match { + case ir"(x: Int) => $body: Int" => fail + }}.isFailure + ) + + ir"(x: Int) => x + 1" match { + case ir"(x: Int) => $f(x): Int" => assert(f =~= ir"(x: Int) => x + 1") + } + // --- + } +} + +object ExtractingTests { + object Embedding extends FastANF +} diff --git a/src/test/scala/squid/ir/fastir/HOPTests.scala b/src/test/scala/squid/ir/fastir/HOPTests.scala new file mode 100644 index 00000000..fc784aaa --- /dev/null +++ b/src/test/scala/squid/ir/fastir/HOPTests.scala @@ -0,0 +1,184 @@ +package squid +package ir +package fastir + +import scala.util.Try + +class HOPTests extends MyFunSuiteBase(HOPTests.Embedding) { + import HOPTests.Embedding.Predef._ + + test("Matching lambda bodies") { + val id = ir"(z:Int) => z" + + ir"(a: Int) => a + 1" match { + case ir"(x: Int) => $body(x): Int" => assert(body =~= ir"(_:Int) + 1") + } + + ir"(a: Int) => a + 1" match { + case ir"(x: Int) => $body(x):$t" => assert(body =~= ir"(_:Int)+1") + } + + ir"(a: Int) => a + 1" match { + case ir"(x: Int) => ($exp(x):Int)+1" => assert(exp =~= id) + } + + assert(Try { + ir"(a: Int, b: Int) => a + 1" match { + case ir"(x: Int, y: Int) => $body(y):Int" => fail + } + }.isFailure) + + ir"(a: Int, b: Int) => a + 1" match { + case ir"(x: Int, y: Int) => $body(x):Int" => + } + + ir"(a: Int, b: Int) => a + 1" match { + case ir"(x: Int, y: Int) => $body(x,y):Int" => + } + + + ir"(a: Int, b: Int) => a + b" match { + case ir"(x: Int, y: Int) => $body(x,y):Int" => assert(body =~= ir"(_:Int)+(_:Int)") + } + + assert(Try { + ir"(a: Int, b: Int) => a + b" match { + case ir"(x: Int, y: Int) => $body(x):Int" => fail + } + }.isFailure) + + ir"(a: Int, b: Int) => a + b" match { + case ir"(x: Int, y: Int) => ($lhs(x):Int)+($rhs(y):Int)" => + assert(lhs =~= id) + assert(rhs =~= id) + } + + assert(Try { + ir"(a: Int, b: Int) => a + b" match { + case ir"(x: Int, y: Int) => ($lhs(y):Int)+($rhs(y):Int)" => + } + }.isFailure) + } + + test("Matching let-binding bodies") { + // Not implemented error in `letin` + //ir"val a = ${ir"0"}; val b = 1; a + b" match { + // case ir"val x: Int = $v; $body(x):Int" => + // assert(v == ir"0") + // body matches { + // case ir"(y:Int) => { val x: Int = $w; $body(x,y):Int }" => + // assert(w == ir"1") + // assert(body == ir"(u:Int,v:Int) => (v:Int)+(u:Int)") + // } + //} + } + + test("Non-trivial arguments") { + val id = ir"(z: Int) => z" + + ir"(a: Int, b: Int, c: Int) => a + b + c" matches { + case ir"(x: Int, y: Int, z: Int) => $body(x + y, z): Int" => assert(body == ir"(r: Int, s: Int) => r + s") + } + + ir"(a: Int, b: Int, c: Int) => a + b + c" match { + case ir"(x: Int, y: Int, z: Int) => $body(x + y + z): Int" => assert(body =~= id) + } + + ir"(a: Int) => readInt + a" match { + case ir"(x: Int) => $body(readInt, x): Int" => assert(body =~= ir"(r: Int, s: Int) => r + s") + } + + ir"(a: Int) => readInt + a" match { + case ir"(x: Int) => $body(x, readInt): Int" => assert(body =~= ir"(r: Int, s: Int) => s + r") + } + + ir"(a: Int, b: Int) => readInt + (a + b)" match { + case ir"(x: Int, y: Int) => $body(readInt, x + y): Int" => assert(body =~= ir"(r: Int, s: Int) => r + s") + } + + ir"List(1,2,3).foldLeft(0)((acc,x) => acc+x)" match { + case ir"$foldLeft(List(1, 2, 3), 0, (acc: Int, x: Int) => acc + x)" => + assert(foldLeft =~= ir"(l: List[Int], z: Int, f: (Int, Int) => Int) => l.foldLeft(z)(f)") + } + + ir"List(0,1,2,3).foldLeft(0)((acc,x) => acc+x)" match { + case ir"$foldLeft(List(0, 1, 2, 3), 0, (acc: Int, x: Int) => acc + x)" => + assert(foldLeft =~= ir"(l: List[Int], z: Int, f: (Int, Int) => Int) => l.foldLeft(z)(f)") + } + + ir"List(0,1,2,3).foldLeft(0)((acc,x) => acc+x)" match { + case ir"$foldLeft(0, List(0, 1, 2, 3), (acc: Int, x: Int) => acc + x)" => + assert(foldLeft =~= ir"(z: Int, _: List[Int], f: (Int, Int) => Int) => List(z, 1, 2, 3).foldLeft(z)(f)") + } + } + + test("Match letbindings") { + ir"val a = 10.toDouble; a + 1" match { + case ir"val x = 10.toDouble; $body(x)" => assert(body =~= ir"(_: Double) + 1") + } + + ir"val a = 11.toDouble; val b = 22.toDouble; val c = 33.toDouble; (a,b,c)" match { + case ir"val a = ($x:Int).toDouble; val b = ($y:Int).toDouble; $body(a, b)" => + assert(x =~= ir"11") + assert(y =~= ir"22") + assert(body =~= ir"(a: Double, b: Double) => (a, b, 33.toDouble)") + } + + //val a = ir"val a = 10.toDouble; val b = a + 1; val c = b + 2; c" matches { + // case ir"val x = 10.toDouble; $body(x):Double" => + // assert(ir"$body(42)" == ir"(val a = (x: Int) => (val b = x + 1; val c = b + 2; c)); val tmp = a.apply(42.0); tmp") + //} + + // val b = ir"val a = 10; val b = readInt; val c = a + b; c" rewrite { + // case ir"val x = 10; val y = readInt; $body(x): Int" => ir"$body(42)" + // // case ir"val x = 10.toDouble; $body(x):Double" => body(ir"readDouble") + // } alsoApply println + // assert(b == ir"readDouble.toInt + 42") + // assert(b == ir"val r = readDouble + 1; r") + } + + test("HOPHoles should be able to extract an impure statement") { + val f = ir"(x: Int) => println(x + 1)" + ir"val a = 20; $f(a)" match { + case ir"(x: Int) => ($prt(x + 1): Int)" => assert(prt =~= ir"(x: Int) => println(x)") + } + } + + test("HOPHoles should extract all the occurences of the pattern") { + ir"val f = (x: Int, y: Int) => { println(x + y); x + y }; f(11, 22)" match { + case ir"(x: Int, y: Int) => ($h(x + y): Int)" => assert(h =~= ir"(x: Int) => { println(x); x }") + } + + ir"val f = (x: Int, y: Int) => { println(x + y); val a = x + y; println(x * y); val b = x * y; a + b}; f(11, 22)" match { + case ir"(x: Int, y: Int) => ($h(x + y, x * y): Int)" => assert(h =~= ir"(x: Int, y: Int) => { println(x); println(y); x + y }") + } + + ir"val f = (x: Int, y: Int) => { println(x * y); val a = x + y; println(x + y); val b = x * y; a + b}; f(11, 22)" match { + case ir"(x: Int, y: Int) => ($h(x + y, x * y): Int)" => assert(h =~= ir"(x: Int, y: Int) => { println(y); println(x); x + y }") + } + + ir"val f = (x: Int, y: Int) => { println(x * y); val a = x + y; println(x + y); val b = x * y; println(a + b); a * b}; f(11, 22)" match { + case ir"(x: Int, y: Int) => ($h(x + y, x * y): Int)" => assert(h =~= ir"(x: Int, y: Int) => { println(y); println(x); println(x + y); x * y}") + } + } + + test("HOPHoles should be able to extract nested letbindings") { + ir"val a = readInt; val b = readInt; val a1 = a + 1; val b1 = b + 1; a1 + b1" match { + case ir"($h(readInt + 1): Int)" => assert(h =~= ir"(x: Int) => x + x") + } + } + + test("HOPHoles should correctly match impure statements") { + ir"val a = readInt; val b = readDouble; a + b" match { + case ir"($h(readInt + readDouble): Double)" => assert(h =~= ir"(x: Int) => x") + } + + ir"val a = readInt; val b = readDouble; b + a" match { + case ir"($h(readInt + readDouble): Double)" => assert(h =~= ir"(x: Int) => { val a = readInt; val b = readDouble; b + a }") + } + } +} + +object HOPTests { + object Embedding extends FastANF +} diff --git a/src/test/scala/squid/ir/fastir/RewritingTests.scala b/src/test/scala/squid/ir/fastir/RewritingTests.scala new file mode 100644 index 00000000..a3e08c44 --- /dev/null +++ b/src/test/scala/squid/ir/fastir/RewritingTests.scala @@ -0,0 +1,262 @@ +package squid +package ir +package fastir + +class RewritingTests extends MyFunSuiteBase(RewritingTests.Embedding) { + import RewritingTests.Embedding.Predef._ + import RewritingTests.Embedding.Quasicodes._ + + test("Simple rewrites") { + val a = ir"123" rewrite { + case ir"123" => ir"666" + } + assert(a =~= ir"666") + + val b = ir"42.toFloat" rewrite { + case ir"42.toFloat" => ir"42f" + } + assert(b =~= ir"val t = ${ir"42"}.toFloat; 42f") + + val c = ir"42.toDouble" rewrite { + case ir"(${Const(n)}: Int).toDouble" => ir"${Const(n.toDouble)}" + } + assert(c =~= ir"val t = ${ir"42"}.toDouble; 42.0") + } + + test("Rewriting subpatterns") { + val a = ir"(10.toDouble + 111) * .5" rewrite { + case ir"(($n: Double) + 111) * .5" => ir"$n * .25" + } + assert(a =~= ir"val t = 10.toDouble; val t2 = (t + 111) * .5; t * .25") + + val b = ir"Option(42).get" rewrite { + case ir"Option(($n: Int)).get" => n + } + assert(b =~= ir"val t = Option(42).get; 42") + + val c = ir"val a = Option(42).get; a * 5" rewrite { + case ir"Option(($n: Int)).get" => n + case ir"($a: Int) * 5" => ir"$a * 2" + } + assert(c =~= ir"val t1 = Option(42); val t2 = t1.get; val t3 = ${ir"42"} * 5; ${ir"42"} * 2") + + val d = ir"val r = (() => readInt.toDouble)(); r * r" rewrite { + case ir"readInt.toDouble" => ir"readDouble" + } + assert(d =~= ir"val r = (() => readDouble)(); r * r") + } + + test("Rewriting with dead-ends") { + val a = ir"Option(42).get; 20" rewrite { + case ir"Option(($n: Int)).get; 20" => n + } + assert(a =~= ir"val t = ${ir"Option(42)"}; 42") + + val b = ir"val r1 = readDouble.toInt; val a = r1 + 2; val b = r1 + 4; r1" rewrite { + case ir"readDouble.toInt" => ir"readInt" + case ir"2" => ir"4" + case ir"4" => ir"8" + } + assert(b =~= ir"val r1 = readInt; val a = r1 + 8; val b = r1 + 8; r1") + + val c = ir"val r1 = readInt; val f = (x: Int) => { val r2 = readInt; val a = x + 1; r2 }; f(r1)" rewrite { + case ir"(x: Int) => { val rX = readInt; val a = x + 1; rX }" => ir"(x: Int) => { val a = x + 22; x }" + } + assert(c =~= ir"val r1 = readInt; val f = (x: Int) => { val a = x + 22; x }; f(r1)") + } + + test("Substitution should be called from inside a reification context") { + val a = ir"readDouble" rewrite { + case ir"readDouble" => ir"42.toDouble" + } + assert(a =~= ir"42.toDouble") + } + + test("Code generation should handle a hole in return position") { + val a = ir"val a = 10.toDouble; val b = 22.toDouble; a + b" rewrite { + case ir"val a = 10.toDouble; ($body:Double)" => ir"readDouble" + } + assert(a =~= ir"readDouble") + } + + test("Rewriting sequences of bindings") { + val a = ir"val a = readInt; val b = readDouble.toInt; a + b" rewrite { + case ir"readDouble.toInt" => ir"readInt" + } + assert(a =~= ir"val a = readInt; val b = readInt; a + b") + } + + test("Rewriting with HOPHoles") { + val a = ir"val a = 11.toDouble; val b = 22.toDouble; val c = 33.toDouble; (a,b,c)" rewrite { + case ir"val a = ($x:Int).toDouble; val b = ($y:Int).toDouble; ($body(a, b): Tuple3[Double, Double, Double])" => + ir"val a = ($x+$y).toDouble/2; $body(a, a)" + } + val f = ir"(x: Double, y: Double) => (x, y, 33.toDouble)" + assert(a =~= ir"val a = (11+${ir"22"}).toDouble/2; $f(a, a)") + } + + test("Rewriting lambdas") { + val a = ir"val a = 11.toDouble; val f = (x: Double) => x * 2; f(a)" rewrite { + case ir"(x: Double) => x * 2" => ir"(x: Double) => x * 4" + } + assert(a =~= ir"val a = 11.toDouble; val fOld = (x: Double) => x * 2; val f = (x: Double) => x * 4; f(a)") + + // Rewrite multiple lambdas + val b = ir"val a = 11.toDouble; val f1 = (x: Double) => { println(x); x * 2 }; val f2 = (x: Double) => { val t = x * 2; val p = println(x); t }; f1(a) + f2(a)" rewrite { + case ir"(x: Double) => { println(x); x * 2 }" => ir"(x: Double) => x * 4" + } + assert(b =~= ir"val a = 11.toDouble; val f1 = (x: Double) => x * 4; val f2 = (x: Double) => x * 4; f1(a) + f2(a)") + + // Rewrite nested lambda + val c = ir"val a = 11.toDouble; val f1 = (x: Double) => { val f2 = (x: Double) => { val t = x * 2; val p = println(x); t }; println(x); f2(x) * 2 }; f1(a)" rewrite { + case ir"(x: Double) => { println(x); x * 2 } " => ir"(x: Double) => x * 4" + } + assert(c =~= ir"val a = 11.toDouble; val f1 = (x: Double) => { val f2 = (x: Double) => x * 4; println(x); f2(x) * 2 }; f1(a)") + + val d = c rewrite { + case ir"(x: Double) => { println(x); val f = (x: Double) => x * 4; f(x) * 2 }" => ir"(x: Double) => x * 4" + } + assert(d =~= ir"val a = 11.toDouble; val f = (x: Double) => x * 4; f(a)") + } + + test("Rewriting should happen at all occurences") { + val a = ir"val a = Option(11).get; val b = 22; val c = Option(33).get; a + b + c" rewrite { + case ir"Option(($n: Int)).get" => n + } + assert(a =~= ir"val a = Option(11).get; val b = 22; val c = Option(33).get; ${ir"11"} + 22 + 33") + + val b = ir"val a = Option(Option(11).get).get; val b = 22; a + b" rewrite { + case ir"Option(($n: Int)).get" => n + } + assert(b =~= ir"val t1 = Option(11).get; val t2 = Option(11).get; ${ir"11"} + 22") + } + + test("Rewriting should remove the dependent pure statements") { + val a = ir"val a = readInt; val b = readDouble.toInt; a + b" rewrite { + case ir"readDouble.toInt" => ir"readInt" + } + assert(a =~= ir"val a = readInt; val b = readInt; a + b") + } + + test("Rewriting should not remove statements that are part of the result") { + val a = ir"val a = readInt; val b = readDouble; a + b" rewrite { + case ir"readInt" => ir"readDouble.toInt" + } + assert(a =~= ir"val a = readDouble.toInt; val b = readDouble; a + b") + } + + test("Rewriting should not partially match inside a lambda") { + val a = ir"(x: Double) => x * 2 + 33" rewrite { + case ir"(x: Double) => x * 2" => fail + } + } + + test("Rewriting should generate ANF valid code") { + val a = ir"List(1,2,3).foldLeft(0)((acc,x) => acc+x) + 4" rewrite { + case ir"($ls: List[Int]).foldLeft[Int]($init)($f)" => + ir"var cur = $init; $ls.foreach(x => cur = $f(cur, x)); cur" + } + assert(a =~= + ir"val t = List(1, 2, 3); val f = ((acc: Int, x: Int) => acc+x); t.foldLeft(0)(f); var cur = 0; t.foreach(x => cur = f(cur, x)); cur + 4") + + val b = ir"4 + List(1,2,3).foldLeft(0)((acc,x) => acc+x)" rewrite { + case ir"($ls: List[Int]).foldLeft[Int]($init)($f)" => + ir"var cur = $init; $ls.foreach(x => cur = $f(cur, x)); cur" + } + assert(b =~= + ir"val t = List(1, 2, 3); val f = ((acc: Int, x: Int) => acc+x); t.foldLeft(0)(f); var cur = 0; t.foreach(x => cur = f(cur, x)); 4 + cur") + } + + test("Rewriting a by-name argument rewrites inside it") { + import RewritingTests.f + + val a = ir"f({val a = 10.toDouble; println(10.toDouble); a})" rewrite { + case ir"10.toDouble" => ir"42.toDouble" + } + assert(a =~= ir"f({val a = 10.toDouble; println(42.toDouble); val b = 10.toDouble; 42.toDouble})") + } + + test("Rewriting with unused statements should inline them") { + val a = ir"val r = readDouble; val a = readInt; val b = r.toInt; b" rewrite { + case ir"readDouble" => ir"val r = readInt; val a = r + 1; 20d" + } + assert(a =~= ir"val r = readInt; val a2 = r + 1; val a1 = readInt; val b = 20d.toInt; b") + } + + test("Rewriting should not duplicate effects") { + val a = ir"val r = readDouble; val b = r.toInt; val c = r.toDouble; b + c" rewrite { + case ir"readDouble" => ir"val r = readInt; val a = r + 1; 20d" + } + assert(a =~= ir"val r = readInt; val a = r + 1; 20d.toInt + 20d.toDouble") + } + + test("Rewriting a non-letbinding with a let-binding") { + val a = ir"val a = 20.toDouble; a + 1" rewrite { + case ir"20" => ir"readInt" + } + assert(a =~= ir"val r = readInt; val a = r.toDouble; a + 1") + + val b = ir"val a = 20.toDouble; a + 1" rewrite { + case ir"20" => ir"val t = readInt; 40" + } + assert(b =~= ir"val t = readInt; val a = 40.toDouble; a + 1") + } + + test("Rewriting should insert the generated code at the right position") { + val a = ir"val f1 = (x: Int) => x + 1; val f2 = (x: Int) => x * 2; f1(f2(5))" rewrite { + case ir"($f1: Int => Int)(($f2: Int => Int)(5))" => ir"($f1 compose $f2)(5)" + } + assert(a =~= ir"val f1 = (x: Int) => x + 1; val f2 = (x: Int) => x * 2; val f3 = f1 compose f2; f3(5)") + + val b = ir"List(1,2,3).map(_ * 2).map(_ * 4)" rewrite { + case ir"($l: List[Int]).map($f1: Int => Int).map($f2: Int => Int)" => ir"$l.map($f2 compose $f1)" + } + assert(b =~= ir"val l = List(1, 2, 3); val f1 = (x: Int) => x * 2; val cbf1 = List.canBuildFrom; val f2 = (x: Int) => x * 4; val cbf2 = List.canBuildFrom; val f3 = f2 compose f1; l.map(f3)") + } + + test("Squid paper") { + + // 3.4 + val a = ir"List(1,2,3).foldLeft(0)((acc,x) => acc+x) + 4" rewrite { + case ir"($ls: List[Int]).foldLeft[Int]($init)($f)" => + ir"var cur = $init; $ls.foreach(x => cur = $f(cur, x)); cur" + } + assert(a =~= + ir"val t = List(1, 2, 3); val f = ((acc: Int, x: Int) => acc+x); t.foldLeft(0)(f); var cur = 0; t.foreach(x => cur = f(cur, x)); cur + 4") + } + + test("Rewriting simple expressions only once") { + /* + * TODO + * goes into an infinite loop since it rewrites + * at the "current" point. So the topdown transformer + * will apply the transformation again on what was rewritten. + */ + //val a = ir"println((50, 60))" rewrite { + // case ir"($x:Int,$y:Int)" => ir"($y:Int,$x:Int)" + // //case ir"(${Const(n)}:Int)" => Const(n+1) + //} + //assert(a =~= ir"println((61,51))") + } + // + //test("Function Rewritings") { + // val a = ir"(x: Int) => (x-5) * 32" rewrite { + // case ir"($b: Int) * 32" => ir"$b" + // } + // assert(a =~= ir"(x: Int) => x - 5") + // + // val b = ir"(x: Int) => (x-5) * 32" rewrite { + // case ir"(x: Int) => ($b: Int) * 32" => dbg_ir"val x = 42; (p: Int) => $b + p" + // } alsoApply println + // + // println(ir"val u = 42; (v: Int) => (u - 5) + v") + // + // assert(b =~= ir"val u = 42; (v: Int) => (u - 5) + v") + //} +} + +object RewritingTests { + object Embedding extends FastANF + + def f(x: => Double) = x +}