forked from oleg-py/better-monadic-for
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
558 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<plugin> | ||
<name>bm4</name> | ||
<classname>com.olegpy.bm4.BetterMonadicFor</classname> | ||
</plugin> |
158 changes: 158 additions & 0 deletions
158
better-monadic-for/src/main/scala/com/olegpy/bm4/BetterMonadicFor.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
package com.olegpy.bm4 | ||
|
||
import scala.tools.nsc | ||
import nsc.Global | ||
import nsc.plugins.Plugin | ||
import nsc.plugins.PluginComponent | ||
import nsc.transform.{Transform, TypingTransformers} | ||
|
||
|
||
class BetterMonadicFor(val global: Global) extends Plugin { | ||
val name = "bm4" | ||
val description = "Remove withFilter / partial matches in for-comprehension" | ||
val components = | ||
new ForRewriter(this, global) :: | ||
new MapRemover(this, global) :: | ||
new TupleRemover(this, global) :: | ||
Nil | ||
|
||
var noUncheckedFilter = true | ||
var noMapIdentity = true | ||
var noTupling = true | ||
var implicitPatterns = true | ||
|
||
val knobs = Map( | ||
"no-filtering" -> "Remove .withFilter from generator desugaring", | ||
"no-map-id" -> "Remove .map(x => x) and .map(_ => ())", | ||
"no-tupling" -> "Change desugaring of = bindings to not use tuples where possible", | ||
"implicit-patterns" -> "Enable syntax for implicit definitions inside patterns" | ||
) | ||
|
||
|
||
override val optionsHelp: Option[String] = Some( | ||
knobs | ||
.map { case (key, help) => | ||
s" -P:$name:$key:(y/n)".padTo(31, ' ') ++ help | ||
} | ||
.mkString(System.lineSeparator) | ||
) | ||
|
||
override def init(options: List[String], error: String => Unit): Boolean = { | ||
val (known, unknown) = options.partition(s => knobs.keys.exists(s.startsWith)) | ||
if (unknown.nonEmpty) { | ||
error(s"Unknown options: ${unknown.mkString(", ")}") | ||
return false | ||
} | ||
|
||
val toBoolean = (txt: String) => txt.toLowerCase match { | ||
case "y" | "yes" | "1" | "true" => true | ||
case "n" | "no" | "0" | "false" => false | ||
case _ => | ||
error(s"Unknown boolean value $txt") | ||
return false | ||
} | ||
|
||
for { | ||
key <- known | ||
_ = if (!key.contains(':')) { | ||
error(s"Option $key does not include the parameter (e.g. $key:y)") | ||
return false | ||
} | ||
Array(prefix, value) = key.split(":", 2) | ||
} prefix match { | ||
case "no-filtering" => noUncheckedFilter = toBoolean(value) | ||
case "no-map-id" => noMapIdentity = toBoolean(value) | ||
case "no-tupling" => noTupling = toBoolean(value) | ||
case "implicit-patterns" => implicitPatterns = toBoolean(value) | ||
} | ||
|
||
noUncheckedFilter || noMapIdentity || noTupling | ||
} | ||
} | ||
|
||
class ForRewriter(plugin: BetterMonadicFor, val global: Global) | ||
extends PluginComponent with Transform with TypingTransformers | ||
with NoUncheckedFilter with ImplicitPatterns | ||
{ | ||
|
||
import global._ | ||
|
||
override def implicitPatterns: Boolean = plugin.implicitPatterns | ||
override val noUncheckedFilter: Boolean = plugin.noUncheckedFilter | ||
|
||
val runsAfter = "parser" :: Nil | ||
override val runsRightAfter = Some("parser") | ||
override val runsBefore = "namer" :: Nil | ||
val phaseName = "bm4-parser" | ||
|
||
def newTransformer(unit: CompilationUnit): Transformer = new MyTransformer(unit) | ||
|
||
class MyTransformer(unit: CompilationUnit) extends TypingTransformer(unit) { | ||
// The magic happens in `unapply` of objects defined in mixed in traits | ||
override def transform(tree: Tree): Tree = tree match { | ||
case NoUncheckedFilter(cleaned) => | ||
transform(cleaned) | ||
case ImplicitPatternDefinition(updated) => | ||
transform(updated) | ||
case _ => | ||
super.transform(tree) | ||
} | ||
} | ||
} | ||
|
||
class MapRemover(plugin: BetterMonadicFor, val global: Global) | ||
extends PluginComponent with Transform with TypingTransformers | ||
with NoMapIdentity | ||
{ | ||
import global._ | ||
|
||
|
||
protected def newTransformer(unit: global.CompilationUnit) = | ||
new MapIdentityRemoveTransformer(unit) | ||
|
||
def noMapIdentity = plugin.noMapIdentity | ||
|
||
val phaseName = "bm4-typer" | ||
val runsAfter = "typer" :: Nil | ||
|
||
|
||
override val runsBefore: List[String] = "patmat" :: Nil | ||
|
||
class MapIdentityRemoveTransformer(unit: CompilationUnit) | ||
extends TypingTransformer(unit) | ||
{ | ||
override def transform(tree: Tree): Tree = { | ||
tree match { | ||
case NoMapIdentity(cleaned) => | ||
transform(cleaned) | ||
case _ => | ||
super.transform(tree) | ||
} | ||
} | ||
} | ||
} | ||
|
||
class TupleRemover(plugin: BetterMonadicFor, val global: Global) | ||
extends PluginComponent with Transform with TypingTransformers | ||
with NoTupleBinding { | ||
import global._ | ||
protected def newTransformer(unit: global.CompilationUnit): Transformer = | ||
new TupleRemoveTransformer(unit) | ||
|
||
def noTupling: Boolean = plugin.noTupling | ||
val phaseName: String = "bm4-parser2" | ||
val runsAfter: List[String] = "parser" :: "bm4-parser" :: Nil | ||
override val runsRightAfter: Option[String] = Some("bm4-parser") | ||
override val runsBefore: List[String] = "patmat" :: Nil | ||
|
||
class TupleRemoveTransformer(unit: CompilationUnit) | ||
extends TypingTransformer(unit) | ||
{ | ||
override def transform(tree: Tree): Tree = tree match { | ||
case NoTupleBinding(cleaned) => | ||
transform(cleaned) | ||
case _ => | ||
super.transform(tree) | ||
} | ||
} | ||
} |
138 changes: 138 additions & 0 deletions
138
better-monadic-for/src/main/scala/com/olegpy/bm4/ImplicitPatterns.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package com.olegpy.bm4 | ||
|
||
trait ImplicitPatterns extends TreeUtils { self => | ||
import global._ | ||
|
||
def implicitPatterns: Boolean | ||
|
||
|
||
|
||
object ImplicitPatternDefinition { | ||
lazy val ut = new NoTupleBinding { | ||
val noTupling: Boolean = false | ||
lazy val global: self.global.type = self.global | ||
} | ||
|
||
def embedImplicitDefs(tupler: Tree, defns: List[ValDef]): Tree = { | ||
val identMap = defns.map { | ||
case vd @ ValDef(_, TermName(nm), _, Ident(TermName(ident))) if nm contains "$implicit" => | ||
ident -> vd | ||
}.toMap | ||
|
||
tupler match { | ||
case Function(vp, Block(valDefns, expr)) => | ||
val withImplicits = valDefns.flatMap { | ||
case vd @ ValDef(_, TermName(nm), _, _) if identMap contains nm => | ||
vd :: identMap(nm) :: Nil | ||
case vd => | ||
vd :: Nil | ||
} | ||
|
||
Function(vp, Block(withImplicits, expr)) | ||
|
||
case Block(valDefns, expr) => | ||
val withImplicits = valDefns.flatMap { | ||
case vd @ ValDef(_, TermName(nm), _, _) if identMap contains nm => | ||
vd :: identMap(nm) :: Nil | ||
case vd => | ||
vd :: Nil | ||
} | ||
|
||
Block(withImplicits, expr) | ||
|
||
case other => | ||
other | ||
} | ||
} | ||
|
||
def unapply(tree: Tree): Option[Tree] = tree match { | ||
case _ if !implicitPatterns => | ||
None | ||
case CaseDef(ImplicitPatternVals(patterns, valDefns), guard, body) => | ||
val newGuard = if (guard.isEmpty) guard else q"{..$valDefns; $guard}" | ||
val replacement = CaseDef(patterns, newGuard, q"{..$valDefns; $body}") | ||
|
||
Some(replaceTree( | ||
tree, | ||
replacement | ||
)) | ||
|
||
case Block((matcher @ NonLocalImplicits(valdefs)) :: stats, expr) => | ||
val m = StripImplicitZero.transform(matcher) | ||
val replacement = embedImplicitDefs(Block(m :: stats, expr), valdefs) | ||
Some(replaceTree(tree, replacement)) | ||
|
||
|
||
case q"$main.map(${tupler @ ut.Tupler(_, _)}).${m @ ut.Untuplable()}(${body @ ut.Untupler(_, _)})" if ForArtifact(tree) => | ||
body match { | ||
case Function(_, Match(_, List(ImplicitPatternVals(_, defns)))) => | ||
val t = StripImplicitZero.transform(embedImplicitDefs(tupler, defns)) | ||
val replacement = q"$main.map($t).$m($body)" | ||
Some(replaceTree(tree, replacement)) | ||
case _ => None | ||
} | ||
|
||
case _ => | ||
None | ||
} | ||
} | ||
|
||
private def mkValDef(nm: String, tnm: Tree): ValDef = { | ||
implicit val fnc = currentFreshNameCreator | ||
ValDef( | ||
Modifiers(Flag.IMPLICIT | Flag.ARTIFACT | Flag.SYNTHETIC), | ||
freshTermName(nm + "$implicit$"), | ||
tnm, | ||
Ident(TermName(nm)) | ||
) | ||
} | ||
|
||
object ImplicitPatternVals { | ||
def unapply(arg: Tree): Option[(Tree, List[ValDef])] = arg match { | ||
case HasImplicitPattern() => | ||
val vals = arg.collect { | ||
case q"implicit0(${Bind(TermName(nm), Typed(_, tpt))})" => | ||
mkValDef(nm, tpt) | ||
} | ||
// We're done with implicit0 "keyword", exterminate it | ||
Some((StripImplicitZero.transform(arg), vals)) | ||
case _ => None | ||
} | ||
} | ||
|
||
object HasImplicitPattern { | ||
def unapply(arg: Tree): Boolean = arg.exists { | ||
case q"implicit0(${Bind(t: TermName, Typed(Ident(termNames.WILDCARD), _))})" if t != termNames.WILDCARD => | ||
true | ||
case q"implicit0($_)" => | ||
reporter.error(arg.pos, "implicit pattern only supports identifier with type pattern") | ||
false | ||
case q"implicit0(..$_)" => | ||
reporter.error(arg.pos, "implicit pattern only accepts a single parameter") | ||
false | ||
case _ => | ||
false | ||
} | ||
} | ||
|
||
object StripImplicitZero extends Transformer { | ||
override def transform(tree: Tree): Tree = tree match { | ||
case q"implicit0(${Bind(t: TermName, _)})" => super.transform(Bind(t, Ident(termNames.WILDCARD))) | ||
case _ => super.transform(tree) | ||
} | ||
} | ||
|
||
object NonLocalImplicits { | ||
def unapply(vd: ValDef): Option[List[ValDef]] = { | ||
vd.rhs match { | ||
case Match(_, CaseDef(pat, _, _) :: Nil) if vd.mods.hasFlag(Flag.ARTIFACT) => | ||
val vd = pat.collect { | ||
case q"implicit0(${Bind(TermName(nm), Typed(_, tpt))})" => | ||
mkValDef(nm, tpt) | ||
} | ||
if (vd.nonEmpty) Some(vd) else None | ||
case _ => None | ||
} | ||
} | ||
} | ||
} |
72 changes: 72 additions & 0 deletions
72
better-monadic-for/src/main/scala/com/olegpy/bm4/NoMapIdentity.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package com.olegpy.bm4 | ||
|
||
import scala.reflect.internal.{Definitions, ModifierFlags, SymbolTable} | ||
import scala.tools.nsc.Global | ||
import scala.tools.nsc.ast.TreeDSL | ||
import scala.tools.nsc.transform.TypingTransformers | ||
|
||
|
||
trait NoMapIdentity extends TreeUtils { | ||
import global._ | ||
|
||
def noMapIdentity: Boolean | ||
|
||
object NoMapIdentity { | ||
def unapply(tree: Tree): Option[Tree] = tree match { | ||
case _ if !noMapIdentity => None | ||
|
||
// plain monomorphic map | ||
case q"${sel @ q"$body.map"}[..$_](${IdentityFunction()})" | ||
if sel.hasAttachment[ForAttachment.type] && | ||
tree.tpe =:= body.tpe => | ||
Some(replaceTree(tree, body)) | ||
|
||
// map with implicit parameters | ||
case q"${sel @ q"$body.map"}[..$_](${IdentityFunction()})(..$_)" | ||
if sel.hasAttachment[ForAttachment.type] && | ||
tree.tpe =:= body.tpe => | ||
Some(replaceTree(tree, body)) | ||
|
||
// map on implicit conversion with implicit parameters (e.g. simulacrum ops) | ||
case q"${sel @ q"$_($body)(..$_).map"}[..$_](${IdentityFunction()})" | ||
if sel.hasAttachment[ForAttachment.type] && | ||
body.tpe.widen =:= tree.tpe // body.tpe will be inferred to singleton type | ||
=> | ||
Some(replaceTree(tree, body)) | ||
case _ => | ||
None | ||
} | ||
} | ||
|
||
object IdentityFunction { | ||
def unapply(tree: Tree): Boolean = tree match { | ||
case Function(ValDef(mods, TermName(x), tpt, _) :: Nil, i @ Ident(TermName(x2))) | ||
if (x == x2) && | ||
mods.flags == ModifierFlags.PARAM && | ||
(i.tpe =:= tpt.tpe) => | ||
true | ||
case Function(ValDef(_, _, tpt, EmptyTree) :: Nil, LiteralUnit(u)) | ||
if tpt.tpe =:= definitions.UnitTpe && tpt.tpe =:= u.tpe => | ||
true | ||
case _ => | ||
false | ||
} | ||
} | ||
|
||
object LiteralUnit { | ||
def unapply(tree: Tree): Option[Tree] = tree match { | ||
case u @ Literal(Constant(())) => | ||
Some(u) | ||
|
||
// In Scala 2:13, we get `(x$1: Unit @unchecked) match { case _ => () }` | ||
case u @ Match(Typed(Ident(TermName(x)), tpt), | ||
List( | ||
CaseDef(Ident(termNames.WILDCARD), EmptyTree, Literal(Constant(()))))) | ||
if tpt.tpe =:= definitions.UnitTpe => | ||
Some(u) | ||
|
||
case _ => | ||
None | ||
} | ||
} | ||
} |
Oops, something went wrong.