Next: Your First Chisel Module
These tutorials use the Jupyter notebook environment so you can read code, make changes, and then run code snippets in place. We encourage you to experiment with tutorial code blocks to speed your way to Chisel mastery.
In this first module, you will learn how to write basic Scala code and how to read more advanced Scala code.
Scala is yet another programming language which supports common programming paradigms. We chose to use it for several reasons:
All of these points will become apparent as we talk about Chisel later, but for now, we are going to focus on the basics of reading and writing Scala code.
Statements that create (mutable) variables and constant values (immutable variables) are preceded with the keywords var and val respectively. It is common practice to use val whenever possible. Why? Mostly to reduce the chances of re-using variables in ways that are error prone or make your code difficult to read. The structure of Scala makes this practice easier than you might expect.
Example:
The subsequent code block is executable right here, within this notebook. To run it, focus on it by clicking it. When the cell is active, a box with a green bar on the left appears around it. Once selected, the active code block cell may be run using the play button found at the top of the notebook (highlighted in red below for reference).
Alternatively, you may use keyboard shortcuts. Shift+enter runs the currently active cell and moves the focus to the next cell. Ctrl+enter runs the current cell and keeps it in focus.
Key examples begin with a blue and bold Example, while exercises begin with a red Exercise. Module 1 consists entirely of short examples, so we've omitted the example declaration for the rest of this module.
var numberOfKittens = 6
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false
One of the first things to notice is that unlike Java and C, Scala does not generally require semicolons at the end of statements. Scala infers the semicolon when there is a line feed. For instance, it can usually tell if a single statement is spread across multiple lines when the last thing on the line is an operater requiring additional code. The only time you need a semicolon is when you want to fit multiple statements onto one line.
You use variables in obvious ways. The two var
s may be reassigned to, while the two val
s are immutable once created.
numberOfKittens += 1
// kittensPerHouse = kittensPerHouse * 2 // This would not compile; kittensPerHouse is not updatable
println(alphabet)
done = true
// A simple conditional; by the way, this is a comment
if (numberOfKittens > kittensPerHouse) {
println("Too many kittens!!!")
}
// The braces are not required when all branches are one liners. However, the
// Scala Style Guide prefers brace omission only if an "else" clause is included.
// (Preferably not this, even though it compiles...)
if (numberOfKittens > kittensPerHouse)
println("Too many kittens!!!")
// ifs have else clauses, of course
// This is where you can omit braces!
if (done)
println("we are done")
else
numberOfKittens += 1
// And else ifs
// For style, keep braces because not all branches are one liners.
if (done) {
println("we are done")
}
else if (numberOfKittens < kittensPerHouse) {
println("more kittens!")
numberOfKittens += 1
}
else {
done = true
}
But in Scala, an "if
" conditional returns a value. What is that value? It's given by the last line of the selected branch. It's quite powerful, particularly when used to initialize values in functions and classes. It looks like:
val likelyCharactersSet = if (alphabet.length == 26)
"english"
else
"not english"
println(likelyCharactersSet)
We created a constant likelyCharactersSet whose value is conditionally determined at runtime.
Methods are defined with the keyword def
. Here, we'll abuse notation and also refer to them as functions. Function arguments (or parameters) are specified in a comma separated list that specifies the name of the argument, its type, and optionally a default value for it. Return types should be specified for clarity.
Scala functions that do not have any arguments do not require empty parentheses. This often makes life easier for a developer in the situation where a member of a class becomes a function because there is some computation associated with referencing it. By convention, argument-less functions that do not have side effects (i.e. calling them does not change anything and they simply return a value) do not use parentheses, and functions that do have side effects (perhaps they change class variables or print stuff out) should require parentheses.
// Simple scaling function with an input argument, e.g., times2(3) returns 6
// Curly braces can be omitted for short one-line functions.
def times2(x: Int): Int = 2 * x
// More complicated function
def distance(x: Int, y: Int, returnPositive: Boolean): Int = {
val xy = x * y
if (returnPositive) xy.abs else -xy.abs
}
The same function name can be used in more than one way. The parameters and their types determine a signature that allows the compiler to figure out which version of the function should be called. Overloaded functions should be avoided, though.
// Overloaded function
def times2(x: Int): Int = 2 * x
def times2(x: String): Int = 2 * x.toInt
times2(5)
times2("7")
Curly braces define code scopes. Within a function's scope may exist more functions or recursive function calls. Functions defined in a certain scope are only accessible within that scope.
/** Prints a triangle made of "X"s
* This is another style of comment
*/
def asciiTriangle(rows: Int) {
// This is cute: multiplying "X" makes a string with many copies of "X"
def printRow(columns: Int): Unit = println("X" * columns)
if(rows > 0) {
printRow(rows)
asciiTriangle(rows - 1) // Here is the recursive call
}
}
// printRow(1) // This would not work, since we're calling printRow outside its scope
asciiTriangle(6)
Scala implements a variety of aggregate or sequence objects. Lists are a lot like arrays but support additional operations for appending and extracting.
val x = 7
val y = 14
val list1 = List(1, 2, 3)
val list2 = x :: y :: y :: Nil // An alternate notation for assembling a list
val list3 = list1 ++ list2 // Appends the second list to the first list
val m = list2.length
val s = list2.size
val headOfList = list1.head // Gets the first element of the list
val restOfList = list1.tail // Get a new list with first element removed
val third = list1(2) // Gets the third element of a list (0-indexed)
for
Statement¶Scala has a for
statement and it works like traditional for statements. You
can iterate over a range of values.
for (i <- 0 to 7) { print(i + " ") }
println()
Use until
instead of to
for iterating from 0 to 6 (7 is not included).
for (i <- 0 until 7) { print(i + " ") }
println()
Add a by
to increment by some fixed amount. The following prints out the even integers between 0 and 10.
for(i <- 0 to 10 by 2) { print(i + " ") }
println()
If you have a collection of some kind and want to visit all of the elements, you can use for
as an iterator, as in Java and Python. Here we make a list of 4 random integers and then sum them.
val randomList = List(scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt())
var listSum = 0
for (value <- randomList) {
listSum += value
}
println("sum is " + listSum)
Scala's for
has a lot more tricks it can do. It will work intuitively for
a wide range of traditional iteration needs, but it may or may not be the most convenient thing to use. Operations like summing the elements of an array are often more easily done using a rich family of functions called comprehensions that are available across many different collections of elements. Later modules will provide more on for
and its allies.
Being able to read Scala code and understand common naming conventions, design patterns, and best practices is an important step in becoming an effective Chisel designer. The potential for code reuse is one of the advantages of Chisel, but reuse is difficult if you can't read other people's code. Effectively parsing other people's code also makes it easier to seek out help, especially from resources like StackOverflow.
The following sections show common code patterns you will see.
package mytools
class Tool1 { ... }
When externally referencing code defined in a file containing the above lines, one should use:
import mytools.Tool1
Note: The package name should match the directory hierarchy. This is not mandatory, but failing to abide by this guideline can produce some unusual and difficult to diagnose problems. Package names by convention are lower case and do not contain separators like underscores. This sometimes makes good descriptive names difficult. One approach is to add a layer of hierarchy, e.g. package good.tools
. Do your best. Chisel itself plays some games with the package names that do not conform to these rules.
As shown above, import
statements inform the compiler that you are using some additional libraries. Some common imports you will use when programming in Chisel are:
import chisel3._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
The first imports all the classes and methods in the chisel3 package; the underscore here works as a wildcard. The second imports specific classes from the chisel3.iotesters package.
Scala is object oriented, and it's important to understand a bit about this to take maxiumum advantage of both Scala and Chisel. Note that, without a doubt, there is more than one way to describe all this.
val
declarative are also objects.val
, var
) associated with the class.We are about to look at how to create a class in Scala.
// WrapCounter counts up to a max value based on a bit size
class WrapCounter(counterBits: Int) {
val max: Long = (1 << counterBits) - 1
var counter = 0L
def inc(): Long = {
counter = counter + 1
if (counter > max) {
counter = 0
}
counter
}
println(s"counter created with max value $max")
}
What is here:
class WrapCounter
-- This is the definition of WrapCounter.(counterBits: Int)
-- Creating a WrapCounter requires an integer parameter, nicely named to suggest it is the bit width of the counter.val max: Long =
-- the class contains a member variable max, declared as type Long
and initialized as the class is created.(1 << counterBits) - 1
computes the maximum value that can be contained in counterBits bits. Since max was created with val
it cannot be changed.counter = counter + 1
increments counter.if (counter > max) { counter = 0 }
tests if the counter is greater than the max value and sets it back to zero if it is.counter
-- The last line of the code block is important.if
then else
statement defines its true and false clauses with code blocks, it can return a value i.e., val result = if (10 * 10 > 90) "greater" else "lesser"
would create a val
with the value "greater".println(s"counter created with max value $max")
prints a string to standard output. Because the println is directly in the defining code block, it is part of the class initialization code and is run, i.e. prints out the string, every time an instance of this class is created.println(s"doubled max is ${max + max}")
.${...}
.Let's use our example above to create a class. Scala instances are created via the built-in magic keyword new
.
val x = new WrapCounter(2)
You may see instances being created without the keyword new
i.e., val y = WrapCounter(6)
.
This occurs often enough to merit special attention, but requires the use of a companion object. It is described later.
Example usage of the instance that has just been created is given next. (Try evaluating the cell below twice.)
x.inc() // Increments the counter
// Member variables of the instance x are visible to the outside, unless they are declared private
if(x.counter == x.max) {
println("counter is about to wrap")
}
x inc() // Scala allows the dots to be omitted; this can be useful for making embedded DSL's look more natural
Code blocks are delimited by braces. A block can contain zero or more lines of Scala code. The last line of Scala code becomes the return value (which may be ignored) of the code block. A code block with no lines would return a special null-like object called Unit
. Code blocks are used throughout Scala: they are the bodies of class definitions, they form function and method definitions, they are the clauses of if
statements, and they are the bodies of for
and many other Scala operators.
Code blocks can take parameters. In the case of class and method definitions, these parameters look like those in most conventional programming languages. In the example below, c
and s
are parameters of the code blocks.
// A one-line code block doesn't need to be enclosed in {}
def add1(c: Int): Int = c + 1
class RepeatString(s: String) {
val repeatedString = s + s
}
IMPORTANT: There is another way in which code blocks may be parameterized. Here is an example.
val intList = List(1, 2, 3)
val stringList = intList.map { i =>
i.toString
}
The code block is passed to a method map
of the class List. The map
method requires that its code block have a single parameter. The code block is called for each member of the list, and the code block returns that member converted to a String. Scala is almost excessively accepting of variations of this syntax. You might see this written in many different ways. This type of code block is called an anonymous function, and more details on anonymous functions are provided in a later module.
The goal here is to help you recognize the different notational types when you encounter them. As you use Scala, these will seem more comfortable and familiar. Authors tend to gravitate to particular styles, and there are also individual syntactical situations in which one notation will seem more natural. One-liners tend to use the more concise forms. Complex blocks usually have a more narrative appearance. To make collaboration easier, one should consider skimming through best practices found in the Scala Style Guide.
Consider the following method definition.
def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... }
When calling the method, you will often see the parameter names along with the passed-in values.
myMethod(count = 10, wrap = false, wrapValue = 23)
Using named parameters, you can even call the function with a different ordering.
myMethod(wrapValue = 23, wrap = false, count = 10)
For frequently called methods, the parameter ordering may be obvious. But for less common methods and, in particular, boolean arguments, including the names with calls can make your code a lot more readable. If methods have a long list of arguments of the same type, using names also decreases the chance of error. Parameters to class definitions also use this named argument scheme (they are actually just the parameters to the constructor method for the class).
When certain parameters have default values (that don't need to be overridden), callers only have to pass (by name) specific arguments that do not use defaults. Notice that the parameter wrapValue
has a default value of 24. Therefore,
myMethod(wrap = false, count = 10)
will work as if 24 had been passed in.