treehugger.scala is a library to write Scala source code programmatically. It’s also an implementation of Scala abstract syntax tree based on Reflection API.
libraryDependencies += "com.eed3si9n" %% "treehugger" % "0.4.3"
resolvers += Resolver.sonatypeRepo("public")
treehugger lets you treat Scala code as data (case classes), and data as code (treehugger DSL) so you can code while you code.
For example, here’s treehugger DSL for Hello World:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree: Tree = Predef_println APPLY LIT("Hello, world!")
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = Apply(Ident(println),List(Literal(Constant(Hello, world!))))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = println("Hello, world!")
The result of the tree
shows the structure,
and treeToString(tree)
shows the Scala source code represented by the tree.
Because it is responsible for formatting of the String
, your code generating code would be more clear using treehugger.
Let’s omit the import statements, and println
statements from here. Here’s how treehugger DSL defines classes:
scala> :paste
// Entering paste mode (ctrl-D to finish)
object sym {
val IntQueue = RootClass.newClass("IntQueue")
val BasicIntQueue = RootClass.newClass("BasicIntQueue")
val buf = BasicIntQueue.newValue("buf")
}
val tree1 = CLASSDEF(sym.IntQueue) withFlags(Flags.ABSTRACT) := BLOCK(
DEF("get", IntClass),
PROC("put") withParams(PARAM("x", IntClass))
)
val tree2 = CLASSDEF(sym.BasicIntQueue) withParents(sym.IntQueue) := BLOCK(
VAL(sym.buf) withFlags(Flags.PRIVATE) :=
NEW(ArrayBufferClass TYPE_OF IntClass),
DEF("get", IntClass) := REF(sym.buf) DOT "remove" APPLY(),
PROC("put") withParams(PARAM("x", IntClass)) := BLOCK(
REF(sym.buf) INFIX("+=") APPLY REF("x")
)
)
// Exiting paste mode, now interpreting.
defined object sym
[1m[34mtree1[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(abstract, , Map()),Modifiers(, , Map()),IntQueue,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),EmptyTree), ProcDef(Modifiers(, , Map()),put,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(x),TypeTree()),EmptyTree))),EmptyTree))))
[1m[34mtree2[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),BasicIntQueue,List(),List(),Template(List(TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(ValDef(Modifiers(private, , Map()),Ident(buf),New(TypeTree())), DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),Ap...
Let’s print them out:
scala> treeToString(tree1)
[1m[34mres1[0m: [1m[32mString[0m =
abstract class IntQueue {
def get: Int
def put(x: Int)
}
scala> treeToString(tree2)
[1m[34mres3[0m: [1m[32mString[0m =
class BasicIntQueue extends IntQueue {
private val buf = new scala.collection.mutable.ArrayBuffer[Int]
def get: Int = buf.remove()
def put(x: Int) {
buf += x
}
}
Notice some of the symbols were defined upfront so we could avoid typing in string names.
To be clear where the credit is due, the majority of treehugger’s code is borrowed from the Scala compiler (scalac
) written by Martin Odersky, Paul Phillips, and others. treehugger just took it further to fit its needs.
Let’s go over the basic compiler theory, so we have some kind of foundation even if it’s overly simplified. A compiler is a program that translates a source code in a language to another, often machine code such as Java bytecode.
The compilers have been written so many times that there are names to each phases of a typical compiler.
Scanning phase, or Lexical Analysis, is responsible for throwing out the white space and comments, and recognizing literals, identifiers, and keywords as tokens. At this point, there is no structural check, except for making sure that the comments and string quotations match up.
Parsing phase, or Syntactic Analysis, is responsible for constructing an abstract syntax tree (AST) from the linear sequence of tokens according to the rules of the grammar of the language.
Typing phase, or Semantic Analysis, is responsible for adding the symbol table to the syntax tree, by associating variables and references with their definitions, and performing type checking.
Analysis and Optimization phase are specific to the language implementation. Some of the common optimizations are inlining and dead code elimination.
Finally, during Code Generation phase the target language is generated either from AST or from an intermediate structure.
Because our end goal is to get Scala source code, we reverse the flow of scalac
to generate the code starting from AST.
Scala 2.10 adds Reflection API, which allows the user peak into AST for a given code. It also includes code that turns AST into Scala source code. All we need now is to generate an AST.
As part of scalac
, there’s also a trait called TreeDSL, which can describe AST in code-like way. With both combined seems as though we have everything we need.
Unfortunately that is not the case. Because scalac
is focused towards Java bytecode generation, the parser expands syntax sugars such as for
loops and infix method applications. In other words, AST contains one of map
, flatMap
, foreach
method in place of a for
loop. TreeDSL too is missing a few things, for example, an ability to define classes or objects.
treehugger forks scalac
’s code base and extends the AST and TreeDSL so all legal Scala code can be described using the DSL (the only exception is XML literals).
Having the scalac
lineage comes with the baggage of having to deal with its data structures.
A Symbol represents an entry in the symbol table, such as values, classes, and type aliases. A compiler would eventually bind all identifiers to a symbol during Typing phase. However, since we are interested in generating the code, the use of symbols are often optional in treehugger.
For example, a unit method declaration may be defined as:
DEF(sym.get)
or:
DEF("get")
Both would yield the same source code at the end. There are several places where the use of symbols are recommended.
First, use a built-in symbol if there is one available. Built-in symbols are defined under treehugger.forest.definitions.
Second, consider defining a symbol for repeated reference to a class or a method. A new symbol may be defined off of an existing symbol as follows:
scala> object sym {
val BasicIntQueue = RootClass.newClass("BasicIntQueue")
val buf = BasicIntQueue.newValue("buf")
val A = ArrowAssocClass.newTypeParameter("A")
val arrow = ArrowAssocClass.newMethod("->")
val B = arrow.newTypeParameter("B")
val T = BasicIntQueue.newAliasType("T")
}
defined object sym
Defining symbols for every identifiers would double the size of the code, and would likely make the experience of writing it cumbersome.
A Type represents a Scala type. Symbols of class ClassSymbol
s and TypeSymbol
s can automatically be promoted to a Type
and many of the built-in symbols represent built-in classes.
VAL("foo", IntClass)
In the above code, IntClass
is a symbol, but it is automatically promoted to a Type
. Similarly, a String
can also be promoted to a Type
.
VAL("foo", "Int")
Types can also be created using type-level expressions and built-in type constructors:
TYPE_ARRAY(StringClass)
TYPE_REF(REF("board") DOT "Coord")
A Tree represents a node in a Scala AST. It could be a simple expression such as a literal, or a combination of other trees.
An expression in treehugger DSL eventually evaluates to a Tree
value.
treehugger DSL is an expanded version of TreeDSL in scalac
to build Scala AST in code-like fashion. It is able to write all legal Scala expressions except for XML literals.
There are several conventions used throughout this document.
VAL(sym|"x")
In the above, sym|"x"
denotes that either a Symbol
or a String
is accepted as a parameter to VAL(...)
.
TYPE_LIST(typ)
In the above, typ
indicates that TYPE_LIST(...)
accepts a Type
.
DEF("x"|sym) withTypeParams(TYPEVAR(...), ...)
In the above, TYPEVAR(...), ...
denotes that withTypeParams(...)
can accept either a vararg list or an Iterable
of TYPEVAR(...)
.
(DEF("get"|sym, [typ])
[withParams(PARAM("x"|sym, typ|"C")[ := arg], ...)]*
[withTypeParams(TYPEVAR(...), ...)]).tree
In the above, [typ]
indicates that typ
is optional; and [withParams(PARAM("x"|sym, typ|"C")[ := arg], ...)]*
indicates that the withParams
clause is optional and may be repeated.
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree: Tree = Predef_println APPLY LIT("Hello, world!")
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = Apply(Ident(println),List(Literal(Constant(Hello, world!))))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = println("Hello, world!")
The entire treehugger system is bundled up as treehugger.Forest
class. The package object for treehugger
defines an instance of Forest
called forest
for convenience. Under the forest
, definitions
object defines built-in symbols and treehuggerDSL
object defines the DSL.
In the above code, object sym
defines optional symbols. By wrapping in sym
we can avoid conflicting with the real println
function. Then, the line defining val tree: Tree
is an example of treehugger DSL.
Finally, forest
defines treeToString
method to convert AST into a String
:
def treeToString(args: Any*): String
treesToString
takes a vararg of Any
, and pretty prints Tree
as Scala source code and everything else using toString
with a new line in between.
We will begin looking into treehugger DSL from the literals and comments since they are the simplest forms of Tree
. Also having these allows us to gradually intoroduce more complex forms off of others.
Literals are the basic foundation of treehugger DSL. Numeric literals, String
, and symbols are written by wrapping a Scala literal with LIT()
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> LIT(1) // Int
[1m[34mres0[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1))
scala> LIT(1L) // Long
[1m[34mres1[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1))
scala> LIT(1.23) // Double
[1m[34mres2[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1.23))
scala> LIT(1.23F) // Float
[1m[34mres3[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1.23))
scala> LIT('H') // Char
[1m[34mres4[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(H))
scala> LIT("H") // String
[1m[34mres5[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(H))
scala> LIT('Sym) // scala.Symbol
[1m[34mres6[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant('Sym))
Boolean literals, ()
, and null
are written as follows:
scala> TRUE // true
[1m[34mres7[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(true))
scala> FALSE // false
[1m[34mres8[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(false))
scala> NULL // null
[1m[34mres9[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(null))
scala> UNIT // ()
[1m[34mres10[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(()))
Single line comments are added to an arbitrary tree as follows:
tree withComment("comments here", ...)
For example,
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = LIT(2) withComment("comments are useful",
"only if they provide more info than the code")
[1m[34mtree[0m: [1m[32mtreehugger.forest.Commented[0m = Commented(Modifiers(, , Map()),List(comments are useful, only if they provide more info than the code),Literal(Constant(2)))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m =
// comments are useful
// only if they provide more info than the code
2
Scaladoc style comments are written using withDoc
as follows:
tree withDoc("comments" | doctag, ...)
where doctag
is a doctag defined as DocTag.See(IntClass)
, DocTag.Author("Somebody")
, etc.
For example,
scala> val tree2 = (DEF("x") := LIT(0)) withDoc(
"does something",
DocTag.See(IntClass))
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Commented[0m = Commented(Modifiers(protected, , Map()),List(does something, @see scala.Int),DefDef(Modifiers(, , Map()),x,List(),List(),TypeTree(),Literal(Constant(0))))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m =
/**
* does something
* @see scala.Int
*/
def x = 0
A definition, such as a function defnition, introduces names that are bound to some expression, block, or a type.
def foo: Int = 1
On the other hand, an abstract declaration introduces only the names and their types. It can be a part of an abstract class definition.
def foo: Int
Abstract value declarations are written by wrapping the name and the type with VAL(...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = (VAL("foo", IntClass): Tree)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = ValDef(Modifiers(, , Map()),Typed(Ident(foo),TypeTree()),EmptyTree)
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = val foo: Int
or in general:
VAL(sym|"x", typ|"C").tree
where sym
is a symbol and typ
is a type of the value. In the above code, sym|"x"
denotes that either a symbol or String
is accepted as the name of the value, and type|"C"
denotes that either a type or String
is accepted as the type of the value.
Calling tree
method creates a Tree
without a right-hand side (rhs) expression. Because there’s an implicit conversion, forcing VAL(...)
as a Tree
automatically calls tree
method.
Value definitions are written by appending right-hand side tree after :=
as follows:
scala> val tree2 = VAL("foo", IntClass) := LIT(0)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.ValDef[0m = ValDef(Modifiers(, , Map()),Typed(Ident(foo),TypeTree()),Literal(Constant(0)))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = val foo: Int = 0
Like Scala, the type annotation can be omitted when rhs is provided:
scala> val tree3 = VAL("foo") := LIT(0)
[1m[34mtree3[0m: [1m[32mtreehugger.forest.ValDef[0m = ValDef(Modifiers(, , Map()),Ident(foo),Literal(Constant(0)))
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m = val foo = 0
In addition, a symbol could be used to define a value instead of using String
names.
scala> object sym {
val foo = RootClass.newValue("foo")
}
defined object sym
scala> val tree4 = VAL(sym.foo) := LIT(0)
[1m[34mtree4[0m: [1m[32mtreehugger.forest.ValDef[0m = ValDef(Modifiers(, , Map()),Ident(foo),Literal(Constant(0)))
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m = val foo = 0
For a larger code base, using symbols makes the code more readable.
The general form of constant value definitions are:
VAL(sym|"x", [typ]) := rhs
Lazy value declarations are written using LAZYVAL(...)
:
scala> val tree5 = (LAZYVAL("foo", IntClass): Tree)
[1m[34mtree5[0m: [1m[32mtreehugger.forest.Tree[0m = ValDef(Modifiers(lazy, , Map()),Typed(Ident(foo),TypeTree()),EmptyTree)
scala> treeToString(tree5)
[1m[34mres4[0m: [1m[32mString[0m = lazy val foo: Int
and lazy value definitions are written as:
scala> val tree6 = LAZYVAL("foo", IntClass) := LIT(0)
[1m[34mtree6[0m: [1m[32mtreehugger.forest.ValDef[0m = ValDef(Modifiers(lazy, , Map()),Typed(Ident(foo),TypeTree()),Literal(Constant(0)))
scala> treeToString(tree6)
[1m[34mres5[0m: [1m[32mString[0m = lazy val foo: Int = 0
Final value definitions are written by appending withFlags(...)
after VAL(...)
:
scala> val tree7 = VAL("foo", IntClass) withFlags(Flags.FINAL) := LIT(0)
[1m[34mtree7[0m: [1m[32mtreehugger.forest.ValDef[0m = ValDef(Modifiers(final, , Map()),Typed(Ident(foo),TypeTree()),Literal(Constant(0)))
scala> treeToString(tree7)
[1m[34mres6[0m: [1m[32mString[0m = final val foo: Int = 0
There is another form of value definitions called pattern values, but it will be discussed later.
Abstract variable declarations are written using VAR(...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = (VAR("foo", IntClass): Tree)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = ValDef(Modifiers(<mutable>, , Map()),Typed(Ident(foo),TypeTree()),EmptyTree)
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = var foo: Int
or in general:
VAR(sym|"x", typ|"C").tree
Value definitions are written as:
scala> val tree2 = VAR("foo", IntClass) := LIT(0)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.ValDef[0m = ValDef(Modifiers(<mutable>, , Map()),Typed(Ident(foo),TypeTree()),Literal(Constant(0)))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = var foo: Int = 0
as a special form of initializing the variable with the default value of the type, WILDCARD
can be used as follows:
scala> val tree3 = VAR("foo", IntClass) := WILDCARD
[1m[34mtree3[0m: [1m[32mtreehugger.forest.ValDef[0m = ValDef(Modifiers(<mutable>, , Map()),Typed(Ident(foo),TypeTree()),Ident(_))
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m = var foo: Int = _
Abstract type declarations are written using TYPEVAR(sym|"T")
. Optionally, lower bounds and higher bounds can be specified:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = (TYPEVAR("T"): Tree)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = TypeDef(Modifiers(, , Map()),T,List(),EmptyTree)
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = type T
scala> val tree2 = (TYPEVAR("T") LOWER(IntClass): Tree)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Tree[0m = TypeDef(Modifiers(, , Map()),T,List(),TypeTree())
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = type T >: Int
scala> object sym {
val T = RootClass.newAliasType("T")
val A = RootClass.newAliasType("A")
val B = RootClass.newAliasType("B")
}
defined object sym
scala> val tree3 = (TYPEVAR(sym.T) UPPER(ComparableClass TYPE_OF sym.T): Tree)
[1m[34mtree3[0m: [1m[32mtreehugger.forest.Tree[0m = TypeDef(Modifiers(, , Map()),T,List(),TypeTree())
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m = type T <: Comparable[T]
or in general:
(TYPEVAR(sym|"T") [LOWER(lo|"C")] [UPPER(hi|"D")]).tree
where lo
denotes an optional lower bound type and hi
upper bound type.
Type alias definitions are written by appending :=
and right-hand side Type
. The alias may accompany type parameters given by withTypeParams(TYPEVAR(...), ...)
:
scala> val tree4 = TYPEVAR("IntList") := TYPE_LIST(IntClass)
[1m[34mtree4[0m: [1m[32mtreehugger.forest.TypeDef[0m = TypeDef(Modifiers(, , Map()),IntList,List(),TypeTree())
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m = type IntList = List[Int]
scala> val tree5 = TYPEVAR("Two") withTypeParams(TYPEVAR(sym.A)) := TYPE_TUPLE(sym.A, sym.A)
[1m[34mtree5[0m: [1m[32mtreehugger.forest.TypeDef[0m = TypeDef(Modifiers(, , Map()),Two,List(TypeDef(Modifiers(, , Map()),A,List(),EmptyTree)),TypeTree())
scala> treeToString(tree5)
[1m[34mres4[0m: [1m[32mString[0m = type Two[A] = (A, A)
or in general:
TYPEVAR(sym|"T") [withTypeParams(TYPEVAR(typ1), ...)] := typ2
Variance annotations for the type parameters are set using COVARIANT(...)
and CONTRAVARIANT(...)
:
scala> val A = RootClass.newTypeParameter("A")
[1m[34mA[0m: [1m[32mtreehugger.forest.TypeSymbol[0m = A
scala> val tree6 = (TYPEVAR("M") withTypeParams(TYPEVAR(COVARIANT(A))): Tree)
[1m[34mtree6[0m: [1m[32mtreehugger.forest.Tree[0m = TypeDef(Modifiers(, , Map()),M,List(TypeDef(Modifiers(, , Map()),A,List(),EmptyTree)),EmptyTree)
scala> val tree7 = (TYPEVAR("M") withTypeParams(TYPEVAR(CONTRAVARIANT(A))): Tree)
[1m[34mtree7[0m: [1m[32mtreehugger.forest.Tree[0m = TypeDef(Modifiers(, , Map()),M,List(TypeDef(Modifiers(, , Map()),A,List(),EmptyTree)),EmptyTree)
The above examples print as:
scala> treeToString(tree6)
[1m[34mres5[0m: [1m[32mString[0m = type M[+A]
scala> treeToString(tree7)
[1m[34mres6[0m: [1m[32mString[0m = type M[-A]
Abstract function declarations are written using DEF(...)
or PROC(...)
. Optionally, parameter lists can be specified using withParams(PARAM(...), ...)
and type parameters can be specified using withTypeParams(TYPEVAR(...), ...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = DEF("get", IntClass).tree
[1m[34mtree[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),EmptyTree)
scala> val tree2 = (DEF("sideEffect", UnitClass) withParams()).tree
[1m[34mtree2[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),sideEffect,List(),List(List()),TypeTree(),EmptyTree)
scala> val tree3 = (PROC("put") withParams(PARAM("x", IntClass))).tree
[1m[34mtree3[0m: [1m[32mtreehugger.forest.ProcDef[0m = ProcDef(Modifiers(, , Map()),put,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(x),TypeTree()),EmptyTree))),EmptyTree)
scala> object sym {
val A = RootClass.newAliasType("A")
val B = RootClass.newAliasType("B")
}
defined object sym
scala> val tree4 = (DEF("compare", BooleanClass)
withTypeParams(TYPEVAR(sym.A))
withParams(PARAM("a", sym.A) := LIT(0))
withParams(PARAM("b", sym.A) := LIT(0)): Tree)
[1m[34mtree4[0m: [1m[32mtreehugger.forest.Tree[0m = DefDef(Modifiers(, , Map()),compare,List(TypeDef(Modifiers(, , Map()),A,List(),EmptyTree)),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(a),TypeTree()),Literal(Constant(0)))), List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(b),TypeTree()),Literal(Constant(0))))),TypeTree(),EmptyTree)
The above examples print as:
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = def get: Int
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = def sideEffect(): Unit
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m = def put(x: Int)
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m = def compare[A](a: A = 0)(b: A = 0): Boolean
In genral, for functions with return type:
(DEF("get"|sym, typ)
[withParams(PARAM("x"|sym, typ|"C")[ := arg], ...)]*
[withTypeParams(TYPEVAR(...), ...)]).tree
For procedures:
(PROC("get"|sym)
[withParams(PARAM("x"|sym, typ|"C")[ := arg], ...)]*
[withTypeParams(TYPEVAR(...), ...)]).tree
Function definitions are written by appending right-hand side tree after :=
as follows:
scala> val tree5 = DEF("get", IntClass) := LIT(0)
[1m[34mtree5[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),Literal(Constant(0)))
scala> treeToString(tree5)
[1m[34mres4[0m: [1m[32mString[0m = def get: Int = 0
The result type can be omitted using DEF(...)
as follows:
scala> val tree6 = DEF("get") := LIT(0)
[1m[34mtree6[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),Literal(Constant(0)))
scala> treeToString(tree6)
[1m[34mres5[0m: [1m[32mString[0m = def get = 0
When rhs is a BLOCK(tree, ...)
you need to use DEFINFER(...)
to differentiate from procedures (PROC
was added in 0.4.0).
scala> val tree7 = DEFINFER("get") := BLOCK(LIT(0))
[1m[34mtree7[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),Block(List(),Literal(Constant(0))))
scala> treeToString(tree7)
[1m[34mres6[0m: [1m[32mString[0m =
def get = {
0
}
Procedure definitions are written using PROC(...)
with BLOCK(tree, ...)
for the rhs:
scala> val tree8 = PROC("write") withParams(PARAM("str", StringClass)) := BLOCK(
LIT(0)
)
[1m[34mtree8[0m: [1m[32mtreehugger.forest.ProcDef[0m = ProcDef(Modifiers(, , Map()),write,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(str),TypeTree()),EmptyTree))),Block(List(),Literal(Constant(0))))
scala> treeToString(tree8)
[1m[34mres7[0m: [1m[32mString[0m =
def write(str: String) {
0
}
Note withParams(...)
clause may be added multiple times, each forming a parameter list:
scala> val tree9 = (DEF("compare")
withTypeParams(TYPEVAR(sym.A))
withParams(PARAM("a", sym.A))
withParams(PARAM("b", sym.A))) := FALSE
[1m[34mtree9[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),compare,List(TypeDef(Modifiers(, , Map()),A,List(),EmptyTree)),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(a),TypeTree()),EmptyTree)), List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(b),TypeTree()),EmptyTree))),TypeTree(),Literal(Constant(false)))
scala> treeToString(tree9)
[1m[34mres8[0m: [1m[32mString[0m = def compare[A](a: A)(b: A) = false
Similar to VAL(...)
, PARAM(...)
can be followed by :=
and rhs to specify the default argument:
scala> val tree10 = (DEF("compare")
withTypeParams(TYPEVAR(sym.A))
withParams(PARAM("a", sym.A) := LIT(0))
withParams(PARAM("b", sym.A) := LIT(0))) := FALSE
[1m[34mtree10[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),compare,List(TypeDef(Modifiers(, , Map()),A,List(),EmptyTree)),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(a),TypeTree()),Literal(Constant(0)))), List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(b),TypeTree()),Literal(Constant(0))))),TypeTree(),Literal(Constant(false)))
scala> treeToString(tree10)
[1m[34mres9[0m: [1m[32mString[0m = def compare[A](a: A = 0)(b: A = 0) = false
By-name parameters are written using TYPE_BYNAME(typ)
as follows:
scala> val tree11 = (PROC("whileLoop")
withParams(PARAM("cond", TYPE_BYNAME(BooleanClass)))
withParams(PARAM("stat", TYPE_BYNAME(UnitClass))): Tree)
[1m[34mtree11[0m: [1m[32mtreehugger.forest.Tree[0m = ProcDef(Modifiers(, , Map()),whileLoop,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(cond),TypeTree()),EmptyTree)), List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(stat),TypeTree()),EmptyTree))),EmptyTree)
scala> treeToString(tree11)
[1m[34mres10[0m: [1m[32mString[0m = def whileLoop(cond: => Boolean)(stat: => Unit)
Repeated parameters are written using TYPE_*(typ)
as follows:
scala> val tree12 = (DEF("sum", IntClass)
withParams(PARAM("args", TYPE_*(IntClass))): Tree)
[1m[34mtree12[0m: [1m[32mtreehugger.forest.Tree[0m = DefDef(Modifiers(, , Map()),sum,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(args),TypeTree()),EmptyTree))),TypeTree(),EmptyTree)
scala> treeToString(tree12)
[1m[34mres11[0m: [1m[32mString[0m = def sum(args: Int*): Int
Import clauses are written using IMPORT(...)
. Optionally, import selectors may be specified:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = IMPORT(MutablePackage)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Import[0m = Import(Ident(mutable),List())
scala> val tree2 = IMPORT("scala.collection.mutable")
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Import[0m = Import(Ident(mutable),List())
scala> val tree3 = IMPORT(MutablePackage, "_")
[1m[34mtree3[0m: [1m[32mtreehugger.forest.Import[0m = Import(Ident(mutable),List(ImportSelector(_,-1,_,-1)))
scala> val tree4 = IMPORT(MutablePackage, "Map", "Set")
[1m[34mtree4[0m: [1m[32mtreehugger.forest.Import[0m = Import(Ident(mutable),List(ImportSelector(Map,-1,Map,-1), ImportSelector(Set,-1,Set,-1)))
scala> val tree5 = IMPORT(MutablePackage, RENAME("Map") ==> "MutableMap")
[1m[34mtree5[0m: [1m[32mtreehugger.forest.Import[0m = Import(Ident(mutable),List(ImportSelector(Map,-1,MutableMap,-1)))
The above examples print as:
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = import scala.collection.mutable
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = import scala.collection.mutable
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m = import scala.collection.mutable._
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m = import scala.collection.mutable.{Map, Set}
scala> treeToString(tree5)
[1m[34mres4[0m: [1m[32mString[0m = import scala.collection.mutable.{Map => MutableMap}
In general:
IMPORT(sym|"x.y.z", ["X" | RENAME("X") ==> "Y"]*)
The only odd thing is ==>
operator used for RENAME(...)
. Because =>
is already taken by Scala, treehugger DSL uses ==>
instead.
Classes and objects are both defined in terms of templates, whose body is represented using BLOCK(...)
in treehugger DSL.
In general, treehugger DSL uses BLOCK(...)
wherever curly braces ({}
) appear in Scala. BLOCK(...)
accepts vararg of trees, such as class member definitions and expressions:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> object sym {
val IntQueue: ClassSymbol = RootClass.newClass("IntQueue")
}
defined object sym
scala> val tree = CLASSDEF(sym.IntQueue) withFlags(Flags.ABSTRACT) := BLOCK(
DEF("get", IntClass),
PROC("put") withParams(PARAM("x", IntClass))
)
[1m[34mtree[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(abstract, , Map()),Modifiers(, , Map()),IntQueue,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),EmptyTree), ProcDef(Modifiers(, , Map()),put,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(x),TypeTree()),EmptyTree))),EmptyTree))))
This example prints as:
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m =
abstract class IntQueue {
def get: Int
def put(x: Int)
}
Class definitions are written using CLASSDEF(...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = (CLASSDEF("C"): Tree)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = class C
scala> val tree2 = CLASSDEF("C") := BLOCK(
VAL("x") := LIT(0)
)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(ValDef(Modifiers(, , Map()),Ident(x),Literal(Constant(0))))))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m =
class C {
val x = 0
}
The general form of the first example is:
CLASSDEF(sym|"C").empty
As with value and function names, CLASSDEF
can accept either a symbol or a String
.
Primary constructor parameters are written using withParams(...)
similar to function definitions. Except, it could use VAL(...)
and VAR(...)
in addition to PARAM(...)
:
scala> val tree3 = (CLASSDEF("C")
withParams(PARAM("x", IntClass),
VAL("y", StringClass),
VAR("z", TYPE_LIST(StringClass))) := BLOCK(
DEF("hi") := LIT("hi")
))
[1m[34mtree3[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(x),TypeTree()),EmptyTree), ValDef(Modifiers(, , Map()),Typed(Ident(y),TypeTree()),EmptyTree), ValDef(Modifiers(<mutable>, , Map()),Typed(Ident(z),TypeTree()),EmptyTree)),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),hi,List(),List(),TypeTree(),Literal(Constant(hi))))))
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m =
class C(x: Int, val y: String, var z: List[String]) {
def hi = "hi"
}
Auxiliary constructors are defined using DEFTHIS := BLOCK(stat, ...)
. Optionally, DEFTHIS
may take withParams(...)
:
scala> val tree4 = (CLASSDEF("C")
withParams(PARAM("s", StringClass)) := BLOCK(
DEFTHIS withParams(PARAM("x", IntClass)) := BLOCK(
THIS APPLY(REF("x") TOSTRING)
)
))
[33mwarning: [0mthere was one feature warning; for details, enable `:setting -feature' or `:replay -feature'
[1m[34mtree4[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(s),TypeTree()),EmptyTree)),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),_$this,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(x),TypeTree()),EmptyTree))),TypeTree(),Block(List(),Apply(This(),List(Select(Ident(x),toString))))))))
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m =
class C(s: String) {
def this(x: Int) = {
this(x.toString)
}
}
To define classes by extending super classes use withParents(tree|typ|"T")
:
scala> val tree5 = CLASSDEF("C") withParents("B") := BLOCK(
DEF("x") := LIT(0)
)
[1m[34mtree5[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),x,List(),List(),TypeTree(),Literal(Constant(0))))))
scala> treeToString(tree5)
[1m[34mres4[0m: [1m[32mString[0m =
class C extends B {
def x = 0
}
To define self type annotations use withSelf(sym|"self", [typ1, ...])
:
scala> val tree6 = CLASSDEF("C") withSelf("self", "T1", "T2") := BLOCK(
VAL("x") := REF("self")
)
[1m[34mtree6[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(),ValDef(Modifiers(, , Map()),Typed(Ident(self),TypeTree()),EmptyTree),List(ValDef(Modifiers(, , Map()),Ident(x),Ident(self)))))
scala> treeToString(tree6)
[1m[34mres5[0m: [1m[32mString[0m =
class C { self: T1 with T2 =>
val x = self
}
To define field values before supertype constructor is called add early definitions using withEarlyDefs(tree, ...)
:
scala> val tree7 = CLASSDEF("C") withEarlyDefs(
VAL("name") := LIT("Bob")
) withParents("B") := BLOCK(
LIT(0)
)
[1m[34mtree7[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(Block(List(),ValDef(Modifiers(, , Map()),Ident(name),Literal(Constant(Bob)))), TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(Literal(Constant(0)))))
scala> treeToString(tree7)
[1m[34mres6[0m: [1m[32mString[0m =
class C extends {
val name = "Bob"
} with B {
0
}
Many of the Tree
objects has Modifier
field, which adds extra attribute about the tree such as access level and mutability. For both classes and their members modifier flags can be given using withFlags(...)
, which takes a PRIVATEWITHIN
or vararg of Long
.
To define class members with access modifiers use Flags.PRIVATE
, Flags.PROTECTED
, and PRIVATEWITHIN("scope")
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = CLASSDEF("C") := BLOCK(
DEF("x") withFlags(Flags.PRIVATE) := LIT(0),
DEF("y") withFlags(Flags.PROTECTED) := LIT(0),
DEF("z") withFlags(PRIVATEWITHIN("this")) := LIT(0)
)
[1m[34mtree[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(private, , Map()),x,List(),List(),TypeTree(),Literal(Constant(0))), DefDef(Modifiers(protected, , Map()),y,List(),List(),TypeTree(),Literal(Constant(0))), DefDef(Modifiers(private[this], , Map()),z,List(),List(),TypeTree(),Literal(Constant(0))))))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m =
class C {
private def x = 0
protected def y = 0
private[this] def z = 0
}
To override class members use Flags.OVERRIDE
:
scala> val tree2 = CLASSDEF("C") withParents("B") := BLOCK(
DEF("x") withFlags(Flags.OVERRIDE) := LIT(0)
)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(override, , Map()),x,List(),List(),TypeTree(),Literal(Constant(0))))))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m =
class C extends B {
override def x = 0
}
To prohibit overriding by subclasses class members are marked final
using Flags.FINAL
:
scala> val tree3 = CLASSDEF("C") := BLOCK(
DEF("x") withFlags(Flags.FINAL) := LIT(0)
)
[1m[34mtree3[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(final, , Map()),x,List(),List(),TypeTree(),Literal(Constant(0))))))
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m =
class C {
final def x = 0
}
To define abstract classes use Flags.ABSTRACT
on CLASSDEF(...)
:
scala> object sym {
val IntQueue: ClassSymbol = RootClass.newClass("IntQueue")
}
defined object sym
scala> val tree4 = CLASSDEF(sym.IntQueue) withFlags(Flags.ABSTRACT) := BLOCK(
DEF("get", IntClass),
PROC("put") withParams(PARAM("x", IntClass))
)
[1m[34mtree4[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(abstract, , Map()),Modifiers(, , Map()),IntQueue,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),get,List(),List(),TypeTree(),EmptyTree), ProcDef(Modifiers(, , Map()),put,List(),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(x),TypeTree()),EmptyTree))),EmptyTree))))
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m =
abstract class IntQueue {
def get: Int
def put(x: Int)
}
To define final classes, which prohibits extension, use Flags.FINAL
on CLASSDEF(...)
:
scala> val tree5 = (CLASSDEF("C") withFlags(Flags.FINAL): Tree)
[1m[34mtree5[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(final, , Map()),Modifiers(, , Map()),C,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> treeToString(tree5)
[1m[34mres4[0m: [1m[32mString[0m = final class C
To define sealed classes use Flags.SEALED
on CLASSDEF(...)
:
scala> val tree6 = CLASSDEF("Animal") withFlags(Flags.ABSTRACT, Flags.SEALED)
[1m[34mtree6[0m: [1m[32mtreehugger.forest.treehuggerDSL.ClassDefStart[0m = treehugger.TreehuggerDSLs$treehuggerDSL$ClassDefStart@428d18c6
scala> treeToString(tree6)
[1m[34mres5[0m: [1m[32mString[0m = treehugger.TreehuggerDSLs$treehuggerDSL$ClassDefStart@428d18c6
To define private constructors use Flags.PRIVATE
with withCtorFlags(...)
on CLASSDEF(...)
:
scala> val tree7 = (CLASSDEF("C") withCtorFlags(Flags.PRIVATE)
withParams(PARAM("x", IntClass)): Tree)
[1m[34mtree7[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(, , Map()),Modifiers(private, , Map()),C,List(),List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(x),TypeTree()),EmptyTree)),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> treeToString(tree7)
[1m[34mres6[0m: [1m[32mString[0m = class C private (x: Int)
Polymorphic classes are defined using withTypeParams(...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = (CLASSDEF("Queue") withTypeParams(TYPEVAR("A"))
withParams(VAL("leading", "A"), VAL("trailing", "A")): Tree)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),Queue,List(TypeDef(Modifiers(, , Map()),A,List(),EmptyTree)),List(ValDef(Modifiers(, , Map()),Typed(Ident(leading),TypeTree()),EmptyTree), ValDef(Modifiers(, , Map()),Typed(Ident(trailing),TypeTree()),EmptyTree)),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = class Queue[A](val leading: A, val trailing: A)
Case classes are defined using CASECLASSDEF(...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> object sym {
val Expr = RootClass.newClass("Expr")
val Var = RootClass.newClass("Var")
}
defined object sym
scala> val tree = (CLASSDEF(sym.Expr) withFlags(Flags.SEALED, Flags.ABSTRACT): Tree)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(sealed abstract, , Map()),Modifiers(, , Map()),Expr,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = sealed abstract class Expr
scala> val tree2 = (CASECLASSDEF(sym.Var)
withParams(PARAM("name", StringClass)) withParents(sym.Expr): Tree)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(case, , Map()),Modifiers(, , Map()),Var,List(),List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(name),TypeTree()),EmptyTree)),Template(List(TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = case class Var(name: String) extends Expr
Traits are defined using TRAITDEF(...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = TRAITDEF("Philosophical") := BLOCK(
PROC("philosophize") := BLOCK(
LIT(0)
)
)
[1m[34mtree[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(<defaultparam/trait>, , Map()),Modifiers(, , Map()),Philosophical,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(ProcDef(Modifiers(, , Map()),philosophize,List(),List(),Block(List(),Literal(Constant(0)))))))
scala> val tree2 = (CLASSDEF("Animal"): Tree)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),Animal,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> val tree3 = (TRAITDEF("HasLegs"): Tree)
[1m[34mtree3[0m: [1m[32mtreehugger.forest.Tree[0m = ClassDef(Modifiers(<defaultparam/trait>, , Map()),Modifiers(, , Map()),HasLegs,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> val tree4 = (CLASSDEF("Frog")
withParents("Animal", "HasLegs", "Philosophical") := BLOCK(
DEF(Any_toString) withFlags(Flags.OVERRIDE) := LIT("green")
))
[1m[34mtree4[0m: [1m[32mtreehugger.forest.ClassDef[0m = ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),Frog,List(),List(),Template(List(TypeTree(), TypeTree(), TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(override <method>, , Map()),toString,List(),List(),TypeTree(),Literal(Constant(green))))))
These print as:
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m =
trait Philosophical {
def philosophize {
0
}
}
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = class Animal
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m = trait HasLegs
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m =
class Frog extends Animal with HasLegs with Philosophical {
override def toString = "green"
}
Object definitions are written using OBJECTDEF(...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = OBJECTDEF("Main") withParents("App") := BLOCK(
LIT(0)
)
[1m[34mtree[0m: [1m[32mtreehugger.forest.ModuleDef[0m = ModuleDef(Modifiers(, , Map()),Main,Template(List(TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(Literal(Constant(0)))))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m =
object Main extends App {
0
}
Case object definitions are written using CASEOBJECTDEF(...)
:
scala> val tree2 = (CASEOBJECTDEF("C"): Tree)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Tree[0m = ModuleDef(Modifiers(case, , Map()),C,Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List()))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = case object C
Now that we’ve covered classes and objects, we should look into expressions in depth.
As we’ve seen earlier, literals are written as follows:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> LIT(1) // 1
[1m[34mres0[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1))
scala> LIT(1L) // 1L
[1m[34mres1[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1))
scala> LIT(1.23) // 1.23
[1m[34mres2[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1.23))
scala> LIT(1.23F) // 1.23F
[1m[34mres3[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(1.23))
scala> LIT('H') // 'H'
[1m[34mres4[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(H))
scala> LIT("H") // "H"
[1m[34mres5[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(H))
scala> LIT('Sym) // 'Sym
[1m[34mres6[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant('Sym))
scala> TRUE // true
[1m[34mres7[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(true))
scala> FALSE // false
[1m[34mres8[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(false))
scala> NULL // null
[1m[34mres9[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(null))
scala> UNIT // ()
[1m[34mres10[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(()))
Simple names are written using REF(sym|"x")
to refer to values and methods that immediately available in the current scope:
scala> object sym {
val x = RootClass.newValue("x")
val y = RootClass.newValue("y")
val Address = RootClass.newClass("Address")
}
defined object sym
scala> REF("x") // x
[1m[34mres11[0m: [1m[32mtreehugger.forest.Ident[0m = Ident(x)
scala> REF(sym.x) // x
[1m[34mres12[0m: [1m[32mtreehugger.forest.Tree[0m = Ident(x)
To refer to other values and methods, selections are written by calling DOT(sym|"y")
either on a symbol or on a REF(sym|"x")
. This returns an intermediate structure that can turn into a Tree
by calling tree
method or by implicit conversion:
scala> (sym.x DOT sym.y).tree // x.y
[1m[34mres13[0m: [1m[32mtreehugger.forest.Select[0m = Select(Ident(x),y)
scala> (sym.x DOT "y": Tree) // x.y
[1m[34mres14[0m: [1m[32mtreehugger.forest.Tree[0m = Select(Ident(x),y)
scala> (REF("x") DOT "y": Tree) // x.y
[1m[34mres15[0m: [1m[32mtreehugger.forest.Tree[0m = Select(Ident(x),y)
scala> val tree = (REF("x") DOT "y").tree
[1m[34mtree[0m: [1m[32mtreehugger.forest.Select[0m = Select(Ident(x),y)
scala> treeToString(tree)
[1m[34mres16[0m: [1m[32mString[0m = x.y
References to this
are written using THIS
or THIS(sym|"C")
:
scala> THIS // this
[1m[34mres17[0m: [1m[32mtreehugger.forest.This[0m = This()
scala> val tree2 = THIS(sym.Address)
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Tree[0m = This(Address)
scala> treeToString(tree2)
[1m[34mres18[0m: [1m[32mString[0m = Address.this
References to super
are written using SUPER
or SUPER(sym|"C")
. This also returns an intermediate structure that can turn into a Tree
by calling tree
method or via implicit conversion:
scala> SUPER.tree // super
[1m[34mres19[0m: [1m[32mtreehugger.forest.Super[0m = Super(EmptyTree,)
scala> (SUPER("C"): Tree) // C.super
[1m[34mres20[0m: [1m[32mtreehugger.forest.Tree[0m = Super(This(C),)
To add type parameter to super
, call APPLYTYPE(sym|"T")
:
scala> val tree3 = SUPER APPLYTYPE "T"
[1m[34mtree3[0m: [1m[32mtreehugger.forest.Super[0m = Super(EmptyTree,T)
scala> treeToString(tree3)
[1m[34mres21[0m: [1m[32mString[0m = super[T]
There are two ways to write function applications. The first way is calling APPLY(arg, ...)
on a symbol or a tree. Here, arg
denotes a Tree
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = Predef_println APPLY LIT("Hello, world!")
[1m[34mtree[0m: [1m[32mtreehugger.forest.Apply[0m = Apply(Ident(println),List(Literal(Constant(Hello, world!))))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = println("Hello, world!")
scala> val tree2 = (REF("x") DOT "y") APPLY (LIT(0), LIT(1))
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Apply[0m = Apply(Select(Ident(x),y),List(Literal(Constant(0)), Literal(Constant(1))))
scala> treeToString(tree2)
[1m[34mres1[0m: [1m[32mString[0m = x.y(0, 1)
The second way is to apply arg, ...
on intermediate structure returned by DOT(sym|"y")
:
scala> val tree3 = (REF("x") DOT "y")(LIT(0), LIT(1))
[1m[34mtree3[0m: [1m[32mtreehugger.forest.Apply[0m = Apply(Select(Ident(x),y),List(Literal(Constant(0)), Literal(Constant(1))))
scala> treeToString(tree3)
[1m[34mres2[0m: [1m[32mString[0m = x.y(0, 1)
To pass sequence into a vararg parameter, use SEQARG(arg)
:
scala> val tree4 = THIS APPLY (SEQARG(REF("list")))
[1m[34mtree4[0m: [1m[32mtreehugger.forest.Apply[0m = Apply(This(),List(Typed(Ident(list),Ident(_*))))
scala> treeToString(tree4)
[1m[34mres3[0m: [1m[32mString[0m = this((list: _*))
To pass named arguments into a function, specify the parameter using REF(sym|"x")
as follows:
scala> val tree5 = REF("put") APPLY (REF("x") := LIT(0))
[1m[34mtree5[0m: [1m[32mtreehugger.forest.Apply[0m = Apply(Ident(put),List(Assign(Ident(x),Literal(Constant(0)))))
scala> treeToString(tree5)
[1m[34mres4[0m: [1m[32mString[0m = put(x = 0)
Partially applied functions are written by calling APPLY
with PARTIALLY
:
scala> val tree6 = REF("put") APPLY PARTIALLY
[1m[34mtree6[0m: [1m[32mtreehugger.forest.Apply[0m = Apply(Ident(put),List(Ident(<partially>)))
scala> treeToString(tree6)
[1m[34mres5[0m: [1m[32mString[0m = put _
Note this is different from APPLY
ing WILDCARD
since PARTIALLY
applies to the entire parameter list.
Type applications are written by calling APPLYTYPE(typ|"C", ...)
on a symbol or a tree:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = REF("put") APPLYTYPE(IntClass) APPLY(LIT(0))
[1m[34mtree[0m: [1m[32mtreehugger.forest.Apply[0m = Apply(TypeApply(Ident(put),List(TypeTree())),List(Literal(Constant(0))))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m = put[Int](0)
There are three ways to write tuple expressions. The most general form is TUPLE(tree, ...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> TUPLE() // ()
[1m[34mres0[0m: [1m[32mtreehugger.forest.Tree[0m = Literal(Constant(()))
scala> TUPLE(REF("x")) // (x)
[1m[34mres1[0m: [1m[32mtreehugger.forest.Tree[0m = Apply(Ident(Tuple1),List(Ident(x)))
scala> TUPLE(LIT(0), LIT(1)) // (0, 1)
[1m[34mres2[0m: [1m[32mtreehugger.forest.Tree[0m = Apply(Ident(Tuple2),List(Literal(Constant(0)), Literal(Constant(1))))
The second way is to use UNIT
literal:
scala> UNIT // ()
[1m[34mres3[0m: [1m[32mtreehugger.forest.Literal[0m = Literal(Constant(()))
Finally, PAREN(tree, ...)
can also be used to write a tuple expression:
scala> PAREN(REF("x")) // (x)
[1m[34mres4[0m: [1m[32mtreehugger.forest.Tree[0m = Apply(Ident(Tuple1),List(Ident(x)))
Semantically speaking the actual scala.Tuple
n are formed only when two or more arguments are passed, but as a syntactic expression, PAREN
is just an alias to TUPLE
.
Instance creations are written using NEW(path|typ|"C")
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> object sym {
val A = RootClass.newClass("A")
}
defined object sym
scala> NEW(sym.A) // new A
[1m[34mres0[0m: [1m[32mtreehugger.forest.Tree[0m = New(TypeTree())
scala> NEW("C") // new C
[1m[34mres1[0m: [1m[32mtreehugger.forest.Tree[0m = New(TypeTree())
scala> NEW(REF("B") DOT "C") // new B.C
[1m[34mres2[0m: [1m[32mtreehugger.forest.Tree[0m = New(Select(Ident(B),C))
Optionally, arguments may be passed into the constructor using NEW(path|typ|"C", arg, ...)
:
scala> val tree = NEW("C", LIT(0), LIT(1))
[1m[34mtree[0m: [1m[32mtreehugger.forest.Tree[0m = Apply(Select(New(TypeTree()),<init>),List(Literal(Constant(0)), Literal(Constant(1))))
scala> treeToString(tree)
[1m[34mres3[0m: [1m[32mString[0m = new C(0, 1)
Anonymous classes are created by passing ANONDEF(parent|"C", ...)
into NEW(...)
:
scala> val tree2 = NEW(ANONDEF() := BLOCK(
DEF("name") := LIT("Robert")
))
[1m[34mtree2[0m: [1m[32mtreehugger.forest.Tree[0m = New(ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),$anon,List(),List(),Template(List(),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),name,List(),List(),TypeTree(),Literal(Constant(Robert)))))))
scala> val tree3 = NEW(ANONDEF("C") := BLOCK(
DEF("name") := LIT("Robert")
))
[1m[34mtree3[0m: [1m[32mtreehugger.forest.Tree[0m = New(ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),$anon,List(),List(),Template(List(TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List(DefDef(Modifiers(, , Map()),name,List(),List(),TypeTree(),Literal(Constant(Robert)))))))
scala> val tree4 = NEW(ANONDEF("C") withEarlyDefs(
VAL("name") := LIT("Robert")
))
[1m[34mtree4[0m: [1m[32mtreehugger.forest.Tree[0m = New(ClassDef(Modifiers(, , Map()),Modifiers(, , Map()),$anon,List(),List(),Template(List(Block(List(),ValDef(Modifiers(, , Map()),Ident(name),Literal(Constant(Robert)))), TypeTree()),ValDef(Modifiers(private, , Map()),Ident(_),EmptyTree),List())))
These examples print as:
scala> treeToString(tree2)
[1m[34mres4[0m: [1m[32mString[0m =
new {
def name = "Robert"
}
scala> treeToString(tree3)
[1m[34mres5[0m: [1m[32mString[0m =
new C {
def name = "Robert"
}
scala> treeToString(tree4)
[1m[34mres6[0m: [1m[32mString[0m =
new {
val name = "Robert"
} with C
Blocks are written using BLOCK(tree, ...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = BLOCK(
VAL("x") := LIT(0),
REF("x")
)
[1m[34mtree[0m: [1m[32mtreehugger.forest.Block[0m = Block(List(ValDef(Modifiers(, , Map()),Ident(x),Literal(Constant(0)))),Ident(x))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m =
{
val x = 0
x
}
Expressions can be constructed from operands and operators.
Prefix operations are written using one of PLUS(tree)
, MINUS(tree)
, NOT(tree)
, or TILDE(tree)
:
PLUS(LIT(1)) // +(1)
MINUS(LIT(1)) // -(1)
NOT(FALSE) // !(false)
TILDE(LIT(1)) // ~(1)
Postfix operations are written by calling POSFIX(sym|"x")
on a tree or a symbol:
LIT(1) POSTFIX(Any_toString) // 1 toString
Infix operations are written using INFIX(sym|"op")
as follows:
LIT(1) INFIX("+") APPLY(LIT(2))
This prints as:
1 + 2
Alternatively, INFIX(sym|"op", arg, ...)
can be called with righ-hand side arguments:
LIT(1) INFIX("+", LIT(2)) // 1 + 2
For commonly used operations treehugger DSL defines some built-in operators, which will be covered later:
LIT(1) INT_+ LIT(2) // 1 + 2
treehugger DSL provides a way to chain infix operations using INFIX_CHAIN(sym|"op", tree, ...)
:
INFIX_CHAIN("+", LIT(1), LIT(2), LIT(3))
INFIX_CHAIN(Any_==, LIT(1) :: LIT(2) :: LIT(3) :: Nil)
The above print as:
1 + 2 + 3
1 == 2 == 3
Typed expressions are written using withType(typ|"C")
:
LIT(0) withType(LongClass) // (0: Long)
Annotated expressions are written using withAnnots(annot, ...)
:
REF("e") withAnnots(ANNOT(UncheckedClass))
This prints as:
(e: @unchecked)
Annotations are covered later in details.
Assignments are written using :=
:
REF("x") := LIT(0) // x = 0
If expressions are written using IF(...) THEN(...) ELSE(...)|ENDIF
as follows:
(IF (REF("x") ANY_== REF("y")) THEN REF("x")
ELSE LIT(0))
IF (REF("sunny")) THEN (Predef_println APPLY LIT("Hi!")) ENDIF
These examples print as:
if (x == y) x
else 0
if (sunny) println("Hi!")
While loops are written using WHILE(...) DO BLOCK(...)
:
WHILE(TRUE) DO BLOCK(
Predef_println APPLY LIT("Hello")
)
This prints as:
```scala while (true) { println(“Hello”) }
Do loops are written by calling DO_WHILE(...)
on a block:
BLOCK(
Predef_println APPLY LIT("Hello")
) DO_WHILE(TRUE)
This prints as:
do {
println("Hello")
} while (true)
For expressions are written using FOR(enum, ...) DO tree
as follows:
FOR(VALFROM("i") := LIT(0) INT_TO LIT(2)) DO (
Predef_println APPLY LIT("Hello"))
FOR(
VALFROM("i") := LIT(0) INT_TO LIT(2),
IF(REF("x") INT_< LIT(10))
) DO (Predef_println APPLY LIT("Hello"))
FOR(
VALFROM("i") := LIT(0) INT_TO LIT(2),
VAL("x") := REF("i")
) DO BLOCK(
Predef_println APPLY LIT("Hello")
)
These examples print as:
for (i <- 1 to 2)
println("Hello")
for {
i <- 0 to 2
if x < 10
} println("Hello")
for {
i <- 0 to 2
x = i
} {
println("Hello")
}
FOR(...)
takes vararg of enumerators which are constructed using VALFROM(sym|"x") := tree
, IF(tree)
, or VAL(sym|"x") := tree
.
For comprehensions are written using FOR(enum, ...) YEILD tree
:
FOR(VALFROM("i") := LIT(0) INT_TO LIT(2)) YIELD (
REF("i"))
This prints as:
for (i <- 0 to 2)
yield i
Return exressions are written using RETURN(tree)
:
RETURN(LIT(0)) // return 0
Exceptions are thrown using THROW(tree)
or THROW(sym|c, [tree|"message"])
:
THROW(REF("x"))
THROW(IllegalArgumentExceptionClass)
THROW(IllegalArgumentExceptionClass, "oh no")
THROW(IllegalArgumentExceptionClass, REF("x"))
These examples print as:
throw x
throw new IllegalArgumentException()
throw new IllegalArgumentException("oh no")
throw new IllegalArgumentException(x.toString)
Exceptions are caught using TRY(stat, ...) CATCH(CASE(pat), ...) [ENDTRY|FINALLY(...)]
. CASE(...)
accepts a pattern matching expression, which is shown briefly here, and covered in detail later:
(TRY (REF("something") APPLY LIT(0))
CATCH (
CASE(WILDCARD) ==> (Predef_println APPLY LIT("error"))
) ENDTRY)
(TRY (REF("something") APPLY LIT(0))
CATCH (
CASE(WILDCARD) ==> (Predef_println APPLY LIT("error"))
) FINALLY(Predef_println APPLY LIT("finally")))
In the above examples, WILDCARD
is a pattern expression that matches to anything. The examples print as:
try {
something(0)
} catch {
case _ => println("error")
}
try {
something(0)
} catch {
case _ => println("error")
} finally println("finally")
Anonymous functions are written using LAMBDA(PARAM(...), ...) ===> rhs
:
LAMBDA(PARAM("x")) ==> (REF("x") INT_+ LIT(1))
LAMBDA(PARAM("x", IntClass)) ==> BLOCK(
REF("x") INT_+ LIT(1))
These examples print as:
x => x + 1
{ (x: Int) =>
x + 1
}
As the second example shows, when the right-hand side tree is a block, the entire anonymous function is wrapped in a block, which makes it convenient to be used with higher-order functions.
The parameter list of anonymous functions may be WILDCARD
:
LAMBDA(PARAM(WILDCARD)) ==> (REF("x") INT_+ LIT(1))
This prints as:
_ => x + 1
In addition, an anonymous functions are formed when an expression contains WILDCARD
.
WILDCARD INT_+ WILDCARD // _ + _
String interpolations are written using INTERP(sym|"x", arg, ...)
:
INTERP(StringContext_s, LIT("Jello"), LIT(1), REF("x"))
INTERP("s", LIT("Hello"), LIT(1), REF("x"))
These examples print as:
s"Jello${1}$x"
s"Hello${1}$x"
Use TYPE_REF(tree|sym|"C")
to explicitly convert symbols, names, and trees into types:
(VAL("pos", TYPE_REF(REF("board") DOT "Coord")): Tree)
This prints as:
val pos: board.Coord
Applied types are written by calling TYPE_OF(typ|"C", ...)
on a type:
REF("x") withType(ListClass TYPE_OF IntClass)
This prints as:
(x: List[Int])
treehugger DSL provides built-in type constructors, which will be covered more later:
REF("x") withType(TYPE_LIST(IntClass))
REF("y") withType(TYPE_TUPLE(IntClass, IntClass))
REF("z") withType(IntClass TYPE_=> IntClass)
These examples print as:
(x: List[Int])
(y: (Int, Int))
(z: Int => Int)
Refined types are written by calling TYPE_WITH (typ|"C", ...)
on a type:
(VAL("x", TYPE_REF("A") TYPE_WITH "B"): Tree)
This prints as:
val x: A with B
Singleton type are written using TYPE_SINGLETON(tree)
:
(VAL("x", TYPE_SINGLETON(THIS)): Tree)
This prints as:
val x: this.type
Structural types are written using TYPE_STRUCT(tree, ...)
:
REF("x") withType(TYPE_STRUCT(
DEF("close", UnitClass)
))
This prints as:
(x: ({ def close: Unit }))
Type projections are written by calling TYPE_# (typ|"C")
on a type:
REF("x") withType(TYPE_STRUCT(
TYPEVAR("L") withTypeParams(TYPEVAR("A")) :=
REF("Const") APPLYTYPE ("M", "A")
) TYPE_#("L"))
This prints as:
(foo: ({ type L[A] = Const[M, A] })#L)
Existential types are written by calling TYPE_FORSOME(tree, ...)
on a type:
(DEF("foo")
withParams(PARAM("arg", TYPE_LIST(
TYPE_REF(REF("x") DOT "T")) TYPE_FORSOME(
VAL("x", "Outer")
)))).tree
This prints as:
def foo(arg: List[x.T] forSome { val x: Outer })
Implicit values are written using withFlags(Flags.IMPLICIT)
:
(DEF("intToRational") withFlags(Flags.IMPLICIT)
withParams(PARAM("x", IntClass)) := NEW("Rational", REF("x")))
This print as:
implicit def intToRational(x: Int) = new Rational(x)
Implicit parameters are also written using withFlags(Flags.IMPLICIT)
:
(DEF("greet")
withParams(PARAM("name", StringClass))
withParams(PARAM("config", "Config")
withFlags(Flags.IMPLICIT)) := BLOCK(
Predef_println APPLY(REF("config") APPLY REF("name"))
))
This prints as:
def greet(name: String)(implicit config: Config) {
println(config(name))
}
View bounds are written by calling VIEWBOUNDS(typ|"T")
on TYPEVAR(...)
:
(DEF("maxList", "T")
withTypeParams(TYPEVAR("T") VIEWBOUNDS TYPE_ORDERED("T"))
withParams(PARAM("elements", TYPE_LIST("T"))): Tree)
This prints as:
def maxList[T <% Ordered[T]](elements: List[T]): T
Context bounds are written by calling CONTEXTBOUNDS(typ|"T")
on TYPEVAR(...)
:
(DEF("put", UnitClass)
withTypeParams(TYPEVAR(sym.A) CONTEXTBOUNDS FullManifestClass)
withParams(PARAM("x", sym.A)): Tree)
This prints as:
def put[A : Manifest](x: A): Unit
Pattern matching is written using tree MATCH(CASE(...), ...)
:
scala> import treehugger.forest._, definitions._, treehuggerDSL._
import treehugger.forest._
import definitions._
import treehuggerDSL._
scala> val tree = (DEF("maxList", "A")
withTypeParams(TYPEVAR("T") VIEWBOUNDS TYPE_ORDERED("A"))
withParams(PARAM("elements", TYPE_LIST("A")))) :=
REF("elements") MATCH (
CASE(ListClass UNAPPLY()) ==>
THROW(IllegalArgumentExceptionClass, "empty list!"),
CASE(ListClass UNAPPLY(ID("x"))) ==> REF("x"),
CASE(ID("x") UNLIST_:: ID("rest")) ==> BLOCK(
VAL("maxRest") := REF("maxList") APPLY(REF("rest")),
IF(REF("x") INT_> REF("maxRest")) THEN REF("x")
ELSE REF("maxRest")
)
)
[1m[34mtree[0m: [1m[32mtreehugger.forest.DefDef[0m = DefDef(Modifiers(, , Map()),maxList,List(TypeDef(Modifiers(, , Map()),T,List(),TypeTree())),List(List(ValDef(Modifiers(<param>, , Map()),Typed(Ident(elements),TypeTree()),EmptyTree))),TypeTree(),Match(Ident(elements),List(CaseDef(UnApply(Ident(List),List()),EmptyTree,Throw(Apply(Select(New(TypeTree()),<init>),List(Literal(Constant(empty list!)))))), CaseDef(UnApply(Ident(List),List(Ident(x))),EmptyTree,Ident(x)), CaseDef(InfixUnApply(Ident(x),$colon$colon,List(Ident(rest))),EmptyTree,Block(List(ValDef(Modifiers(, , Map()),Ident(maxRest),Apply(Ident(maxList),List(Ident(rest))))),If(Infix(Ident(x),>,List(Ident(maxRest))),Ident(x),Ident(maxRest)))))))
scala> treeToString(tree)
[1m[34mres0[0m: [1m[32mString[0m =
def maxList[T <% Ordered[A]](elements: List[A]): A =
elements match {
case List() => throw new IllegalArgumentException("empty list!")
case List(x) => x
case x :: rest => {
val maxRest = maxList(rest)
if (x > maxRest) x
else maxRest
}
}
Let’s look into the pattern expressions.
Variable patterns are written as ID(sym|"x")
and WILDCARD
. They both match any value:
ID("x") // x
WILDCARD // _
Typed patterns are written as pat withType(typ|"C")
:
ID("x") withType(IntClass) // (x: Int)
WILDCARD withType(IntClass) // (_: Int)
Pattern binders are written as pat withBinder(sym|"x")
. Binders are used to give names to patterns:
WILDCARD withBinder("x") // (x @ _)
Literal patterns are written using LIT(...)
:
LIT("X") // "X"
Stable identifier patterns are written using BACKQUOTED(sym|"x")
:
BACKQUOTED("x") // `x`
Constructor patterns are written by calling UNAPPLY(pattern, ...)
to a symbol or a tree:
REF("Address") UNAPPLY(WILDCARD, WILDCARD, WILDCARD)
This prints as:
Address(_, _, _)
Tuple patterns are written using TUPLE(pattern, ...)
:
TUPLE(LIT(0), LIT(1)) // (0, 1)
Sequence wildcards are written using SEQ_WILDCARD withBinder(sym|"x")
:
REF("C") UNAPPLY(SEQ_WILDCARD withBinder("xs"))
This prints as:
C((xs @ _*))
Infix operations patterns are written by calling INFIX(op) UNAPPLY(pat, ...)
to a symbol or a tree:
LIT(0) INFIX(ConsClass) UNAPPLY (NIL)
This prints as:
0 :: Nil
Because this pattern appears frequently, treehugger DSL provides a built-in constructor UNLIST_::
:
LIT(0) UNLIST_:: NIL // 0 :: Nil
This works because an infix operation pattern p op (q, …) is a shorthand for the constructor pattern op(p, q, …).
Pattern alternatives are written as pat1 OR_PATTERN pat2
:
LIT("New York") OR_PATTERN LIT("Paris")
This prints as:
"New York" | "Paris"
Pattern matching expressions are written by calling MATCH(CASE(pattern), ...)
on a symbol or a tree:
REF("x") MATCH(
CASE (LIT(0) OR_PATTERN LIT(1)) ==> TRUE,
CASE (WILDCARD) ==> FALSE
)
This prints as:
x match {
case 0 | 1 => true
case _ => false
}
Just as we saw in anonymous functions, treehugger DSL uses ==>
to denote Scala’s =>
.
Optinally, a guard clause may be added to the case clause using IF(...)
:
REF("x") MATCH(
CASE (ID("x"),
IF(REF("x") INT_< LIT(10))) ==> TRUE,
CASE (WILDCARD) ==> FALSE
)
This prints as:
x match {
case x if x < 10 => true
case _ => false
}
Case sequence functions are defined by listing CASE(...)
clauses in a BLOCK(...)
:
BLOCK(
CASE(SOME(ID("x"))) ==> REF("x"),
CASE(NONE) ==> LIT(0)
)
This prints as:
{
case Some(x) => x
case None => 0
}
Pattern values are defined by placing a pattern in VAL(...)
or VAR(...)
:
VAL(REF("Address") UNAPPLY
(ID("name"), ID("street"), ID("city"))) := REF("x")
VAR(SOME(ID("y"))) := SOME(LIT(1))
These examples print as:
val Address(name, street, city) = x
var Some(y) = Some(1)
Top-level definitions consists of compilation units, packagings, and package objects.
Compilation units are written by calling inPackage(sym|"p")
or withoutPackage
on BLOCK(...)
:
BLOCK(
OBJECTDEF("M")
) inPackage("p")
This prints as:
package p
object M
BLOCK(
OBJECTDEF("M1"),
OBJECTDEF("M2")
) withoutPackage
This prints as:
object M1
object M2
Packagings are written using PACKAGE(sym|"p")
as follows:
PACKAGE("p") := BLOCK(
OBJECTDEF("M")
)
This prints as:
package p {
object M
}
Package objects are written using PACKAGEOBJECTDEF(...)
:
PACKAGEOBJECTDEF("p") := BLOCK(
OBJECTDEF("M")
)
This prints as:
package object p {
object M
}
Annotations are applied to definitions, declarations, types, or expressions.
Annotations are applied to declarations using withAnnots(annot, ...)
. This takes vararg of AnnotationInfo
, which is created using ANNOT(typ|"C", arg, ...)
.
CLASSDEF("C")
withAnnots(ANNOT(SerializableAttr)) := BLOCK(
DEF("get", IntClass) := LIT(0)
)
This prints as:
@serializable class C {
def get: Int = 0
}
Type annotations are written by calling withAnnots(annot, ...)
on TYPEVAR(...)
.
val annot = ANNOT(SpecializedClass, REF(IntClass))
TRAITDEF("Function0")
withTypeParams(TYPEVAR("T") withAnnots(annot)) := BLOCK(
DEF("apply", "T")
)
This prints as:
trait Function0[@specialized(Int) T] {
def apply: T
}
Annotated expressions are written as tree withAnnots(annot, ...)
:
REF("e") withAnnots(ANNOT(UncheckedClass))
This prints as:
(e: @unchecked)
treehugger DSL provides some built-in functions for commonly used functions.
tree TOSTRING // x.toString
tree GETCLASS // x.getClass
tree IS typ // x.isInstanceOf[Int]
tree AS typ // x.asInstanceOf[Int]
lhs ANY_== rhs // ==
lhs ANY_!= rhs // !=
lhs ANY_-> rhs // ->
lhs OBJ_EQ rhs // eq
lhs OBJ_NE rhs // ne
Here are the operators that form infix application tree when called on a symbol or a tree.
lhs OR rhs // ||
lhs AND rhs // &&
lhs INT_| rhs // |
lhs INT_& rhs // &
lhs INT_< rhs // <
lhs INT_> rhs // >
lhs INT_<= rhs // <=
lhs INT_>= rhs // >=
lhs INT_+ rhs // +
lhs INT_- rhs // -
lhs INT_* rhs // *
lhs INT_/ rhs // /
lhs INT_TO rhs // TO
lhs LIST_:: rhs // ::
lhs LIST_::: rhs // :::
lhs SEQ_++ rhs // ++
lhs SEQ_/: rhs // /:
lhs SEQ_:\ rhs // :\
When the right-hand side expression is an anonymous function block or a case sequence function, the collection operators form an infix application tree. Otherwise, they form a selection or an application tree:
REF("list") MAP LAMBDA(PARAM("x")) ==> BLOCK(
REF("x") INT_+ LIT(1)
)
REF("list") MAP (WILDCARD INT_+ LIT(1))
These print as:
list map { x =>
x + 1
}
foo.map(_ + 1)
Here are the operators:
lhs FOREACH rhs // foreach
lhs MAP rhs // map
lhs FLATMAP rhs // flatMap
lhs COLLECT rhs // collect
lhs FIND rhs // find
lhs SLICE (from, to) // slice
lhs TAKE rhs // take
lhs DROP rhs // drop
lhs TAKEWHILE rhs // take
lhs DROPWHILE rhs // drop
lhs FILTER rhs // filter
lhs WITHFILTER rhs // withFilter
lhs FILTERNOT rhs // filterNot
lhs SPLITAT rhs // splitAt
lhs SPAN rhs // span
lhs PARTITION rhs // partition
lhs GROUPBY rhs // groupBy
lhs FORALL rhs // forall
lhs EXISTS rhs // exists
lhs COUNT rhs // count
lhs FOLDLEFT rhs // foldLeft
lhs FOLDRIGHT rhs // foldRight
lhs REDUCELEFT rhs // reduceLeft
lhs REDUCERIGHT rhs // reduceRight
Here are built-in constructors and values:
LIST(arg, ...) // List
NIL // Nil
pat1 UNLIST_:: pat2 // ::
SOME(arg, ...) // Some
NONE // None
RIGHT(arg) // Right
LEFT(arg) // Left
ARRAY(arg, ...) // Array
SEQ(arg, ...) // Seq
VECTOR(arg, ...) // Vector
MAKE_MAP(k ANY_-> v, ...) // Map
Here are built-in type constructors:
TYPE_ITERATOR(typ) // Iterator[A]
TYPE_TUPLE(typ, ...) // Tuple2[A, B, ...]
TYPE_ARRAY(typ) // Array[A]
TYPE_LIST(typ) // List[A]
TYPE_SEQ(typ) // Seq[A]
TYPE_VECTOR(typ) // Vector[A]
TYPE_MAP(typ1, typ2) // Map[A, B]
TYPE_OPTION(typ) // Option[A]
TYPE_EITHER(typ1, typ2) // Either[A, B]
TYPE_RIGHT(typ1, typ2) // Right[A, B]
TYPE_LEFT(typ1, typ2) // Left[A, B]
TYPE_SOME(typ) // Some[A]
TYPE_ORDERED(typ) // Ordered[A]
typ1 TYPE_=> typ2 // Function1[A, R]
TYPE_FUNCTION(typ, ..., r) // Function1[A, R]
TYPE_=:=(typ1, typ2) // =:=[A, B]
TYPE_<:<(typ1, typ2) // <:<[A, B]
TYPE_<%<(typ1, typ2) // <%<[A, B]