Homework 12 and 13: the
last homework
This homework comes in three parts:
lambdas
: exercises of varying difficulty that allow
you to practice using C++ lambdas. It comes with several samples that
you can work from to come up with your solutions. Two exercises are
required, two are BONUS exercises.
circular
: two exercises that have you make a queue
using a (resizeable) circular buffer, but where the underlying storage
is a std::vector
. This comes with my solution the the last
homework. You can either modify mine, or your own, to make your class.
As a BONUS, you can write this code as a template class.
syntaxTree
: this gives an example of a class
hierarchy describing an arithmetic expression over integer variables.
Your job is to invent a similar hierarchy for boolean expressions over
condition variables. You can think of these classes as providing a
syntax tree for such expressions. These use C++ smart
pointers.
In addition to these, there are three folders with
BONUS exercises for you to take a look at, should you
be interested in working on them. These were drafts of projects that I
had hoped to assign, but they relied on modern C++ that we only reached
just after Thanksgiving. Instead, they are BONUS
exercises that you may tackle, or at least take a look at as FYIs.
logicSim
: a single program assignment where you are
to finish the code of a program that simulates circuits that don’t have
feedback. A few pieces of code need to be completed for it to
work.
cobra
: an interpreter for a programming language
Cobra that is very similar to Python. It only allows
calculations on integers, and output of either integers or of text. The
syntax is like Python’s but, rather than relying on indentation to
denote block of code, relies on begin...end
keywords that
mimic the curly braces of C++ ({...}
). You can add new
features to the language by writing methods for how they execute and
compute values. Execution and evaluation involves traversing a syntax
tree built from parsing a program’s code.
cobrac
: a compiler for Cobra that outputs MIPS code
that can be executed in SPIM. You can add new features to the language
by writing methods for converting a syntax tree of any new construct
into lines of MIPS code.
Part 1: Lambdas
Here I ask you to practice writing functions using the
functional
component of the C++ Standard Template Library,
particularly its definition of a “lambda” construct. In C++ you can
express an anonymous function with the expression:
[captures](parameters) -> return-type { body }
In the above, the body
and the parameters
look as they might in a normal function definition. For example, this
function computes the integer whose digits are the two given
integers:
[](unsigned int tens, unsigned int ones) -> unsigned int { return tens*10+ones; }
Note that the return-type
is just the type that would
normally sit at the start of a normal function definition. The
captures
is a list of variables within the same scope of
the lambda expression that are used within the body
. They
can either be “captured” by value (i.e. by copy), in
which case only the variable name is listed, or by reference,
in which case the variable name is listed with an &
symbol at its front. As an example, consider the following:
std::vector<int> v {10,3,1,19};
int i = 3;
std::function<bool(int)> f = [&v,i](int j) -> bool {return v[i]==v[j];};
i--;
std::cout << f(3) << std::endl;
This code will output 1 (for true
) because
f
checks whether the value of v
at the index
of its argument is the same as the value v[3]
. This is
because i
is passed by value, and so v[i]
means v[3]
at the time that f
is expressed. If
the third line was instead:
std::function<bool(int)> f = [&v,&i](int j) -> bool {return v[i]==v[j];};
then the output f(3)
would instead be 0 (for
false
) because f
would be checking
v[2]
on the left side of the ==
. We would need
to call f(2)
instead to get the result
true
.
Note the std::function
type of the lambda in the
declaration of f
above. In general, if a lambda takes a
series of arguments of type T1
, T2
, up to
Tk
and returns something of type T
, then its
type is
std::function<T(T1,T2,...,Tk)>
It is also possible for a lambda to “mutate” the state of variables
captured by value in its capture list by including the keyword
mutable
before the arrow to the return type. For
example,
int sum = 0;
std::function<int(int)> accum =
[sum](int v) mutable -> void {sum += v; return sum;});
std::cout << accum(7) << std::endl;
std::cout << accum(30) << std::endl;
std::cout << accum(500) << std::endl;
This will output three lines with 7
, 37
,
and 537
. This is because, though sum
is
copied, the sum
variable for accum
is allowed
to be changed because of the mutable
keyword. The variable
sum
outside the lambda stays at value 0
, but
the one created by the capture changes.
Here is another example of capture by reference:
std::vector<int> v {10,3,1,19};
int sum = 0;
std::for_each(v.begin(),v.end(),[&sum](int e)->void{sum += e;});
std::cout << sum << std::endl;
The third line above feeds a lambda to the algorithm
package’s for_each
construct. This calls our lambda on each
element value e
of the vector v
. Since
sum
is captured by reference, the +=
operation
bumps the value of sum
by the value of each element of
v
. This results in the output of 33
by the
code.
This example highlights the common use of lambda expressions: they
are a convenient way of working with the algorithms
component of the C++ STL. Incidentally, the lambda expression isn’t
entirely necessary for using this library. For example, we could have
instead developed this class
class Summer {
private:
int& _sum;
public:
Summer(int& s) : _sum{s} {}
void operator()(int e) { _sum += e; }
};
which overloads the “call” operator. It has an “int
reference” attribute. With this class definition, we could have instead
written
std::for_each(v.begin(),v.end(),Summer {sum});
to obtain that same output of sum
of
33
.
This class definition suggest the work done by C++ behind the scenes
to develop every anonymous function object expressed, semantically
speaking.
Part 1 Exercises
For each of these, write testing code in a main
inside a
C++ source file named lambdas/exN.cc
, where N
is the exercise number. The code can be similar to the
lambdas/sampleN.cc
files I’ve included in this repo.
Note: The last two of these are somewhat tricky,
especially in getting the type information correct. They are BONUS
exercises.
Exercise 1.1.
Write and test the use of a lambda that computes the square of its
integer parameter. It shouldn’t capture any variables. Show it working
on several different parameter inputs. For example, when given
9
, it should return 81
.
Exercise 1.2.
Write a lambda that captures a local variable int base
and
a local variable that’s a vector digits
of integers. That
vector should have elements that are only 0
s and
1
s. It should take no parameters.
When called, it should output to std::cout
a line with a
single integer to the console. That value should be the integer formed
from that integer sequence interpreted as digits in that base.
For example, if the base is 2 and the digits vector was initialized
with the sequence
{1,0,1,1,1}
that lambda should output 23
because, interpreting those
as bits from most- to least-significant, the lambda would calculate the
number whose binary sequence is 10111
. (23 is just 16 + 4 +
2 + 1).
If the base were changed to 10, then calling that same lambda object
should output 10111
. If we then called
pop_back
on the digits vector, then that same lambda object
should then output 1011
. If, after that, we changed the
base back to 2, then it would ouput 11
. (11 is just
8+2+1).
Note that the type of this lambda is
std::function<void()>
. It takes no parameters and
returns no result. It outputs to std::cout
instead.
BONUS: Exercise 1.3.
Write a lambda that takes a parameter f
that is a function
of type int(int)
, and a second parameter
int x
. Its capture list should be empty. When that lambda
object is called, it should apply f
twice to that given
x
and return that result of type int
.
Test it with the squaring function you wrote in Exercise 1. For
example, if you wrote the squaring lambda by giving it the name
sqr
, and then wrote this lambda giving it the name
apply_twice
, then
std::cout << apply_twice(sqr,3) << std::endl;
should lead to output of 81
because the square of the
square of 3 is 81.
You can also demonstrate it by writing an “add 10” function lambda.
In that case,
std::cout << apply_twice(add10,3) << std::endl;
should output 23
.
BONUS Exercise 1.4.
Write a lambda that takes a parameter f
that is a function
of type int(int)
. It should take no other parameters. It
should instead give back a function of type
int(int)
. In other words, the body of the lambda should
return a lambda!
Name this lambda object twice
, and demonstrate it by
creating several functions. For example, you could redo the work of
testing Exercise 3 and try out lines like the following:
std::function<int(int)> quad = twice(sqr);
std::function<int(int)> add20 = twice(add10);
and then test these functions on several integer parameters.
Hint: You may have to pass arguments by reference,
and you may have to capture variables by reference. Think it
through!
Part 2: circular buffer as a
vector
In the last homework, you were asked to devise a queue data structure
that was represented as a fixed-capacity array of integers. It took a
capacity in its constructor and then allocated a C array of integers on
the heap of that size. I’ve included my solution for this exercise as a
folder named hw12soln
.
Part 2 Exercise
Excercise 2
Modify this solution’s code, or else the solution that you wrote, so
that it instead uses a std::vector<int>
to store the
data of the queue rather than a C array. There should be one additional
feature: when too many data items have been placed into the queue with
enqueue
, the underlying std::vector
should be
resized to twice its capacity, and then items should be moved around
accordingly to preserve the “circular structure”, and then that extra
item should get enqueued.
Part 3: syntax trees
for boolean conditions
I’ve included a sub-folder named syntaxTree
in this
repo. It contains three files Arithmetic.hh
,
Arithmetic.cc
, and arith.cc
. The first of
these defines a class hierarchy for arithmetic expressions, expressions
like
((8 + (-y)) * (x + z))
These are represented as a tree of subexpressions that could be
depicted like so:
|
|
Times
/ \
/ \
/ Plus
/ / \
/ / \
Plus LookUp LookUp
/ \ "x" "z"
/ \
Number Negation
8 |
LookUp
"y"
The components of the tree are all pointers to objects that are all
derived from the subclass Expression
, namely
Plus
, Times
, Negation
,
Number
, and Lookup
. At the top we have a
pointer to a Times
object that has pointers to a left
subtree built as a Plus
object. That, in turn, has a
pointer to a Number
object, holding the value 8, and to a
Negation
object. The latter has a pointer to a
LookUp
object for the variable named y
. This
whole left subtree from the Times
root corresponds to the
subexpression left of the *
sign:
(8 + (-y))
The right subexpression past the *
sign
(x + z)
corresponds to the right subtree under Times
with
subtree objects Plus
and Lookup
.
The Arithmetic.hh
file defines all these syntax tree
subclasses:
• class Plus
: a syntax tree corresponding to
additions.
• class Times
: a syntax tree corresponding to
multiplications.
• class Negation
: a syntax tree corresponding to a negated
subexpression.
• class Number
: a syntax tree for a constant integer
value.
• class LookUp
: a syntax tree referencing a variable’s
value.
In addition to their constructors, these all support two methods:
• std::string to_string()
: this converts a syntax tree
to a string of its arithmetic expression.
• int evaluate(variables)
: this method evaluates the syntax
tree using
an unordered_map
of variables and their assigned values.
These methods are all defined in Arithmetic.cc
. There is a
client arith.cc
that builds the expression tree for the
above example, prints it out using to_string
, and then
evaluates it using evaluate
on a dictionary of containing
values of x
, y
, and z
. They are
set to 2, 3, and 4, respectively.
It can be compiled and run with the two command lines:
g++ -std=c++11 -o arith arith.cc Arithmetic.cc
./arith
You’ll get the following output:
With x set to 2, y set to 3, and z set to 4,
the value of ((8 + (-y)) * (x + z)) is 30.
For the exercise, I’m asking you to mimic what I’ve done, but to
instead handle logical (boolean) expressions over variables that are
either set true
or false
.
Part 3 Exercise
Exercise 3: make a similar class hierarchy as files
named Condition.hh
and Condition.cc
. Instead
of arithmetic expressions, it should define syntax trees for boolean
conditions on boolean variables.
• class Or
: a syntax tree corresponding to boolean
disjunction.
• class And
: a syntax tree corresponding to boolean
conjuction.
• class Not
: a syntax tree corresponding to a boolean
negation.
• class TruthValue
: a syntax tree for a constant boolean
value, either true
or false
.
• class Variable
: a syntax tree referencing a condition
variable’s truth value.
These should all be subclasses of an abstract superclass named
Condition
. In addition to their constructors, they each
support the methods:
• std::string to_string()
: this converts a syntax tree
to a string of its arithmetic expression.
• bool check(variables)
: this method determines whether or
not the syntax tree is true
given an assignment of truth
values to the given variables
map.
Also write a sample client condi.cc
that builds sample
boolean condition trees and checks whether or not they are
true
or false
.
BONUS Part 4: a Logic
Simulator
The logicSim
folder contains a nearly implemented
combinatorial circuit simulator named logicSim
. It can be
compiled with the command:
g++ -o logicSim -std=c++17 Gate.cc Circuit.cc logicSim.cc
When run with a specified filename, it reads a text file that
specifies the components and wiring of a circuit. When your complete
code is written, it should output the truth table for that circuit. The
folder logicSim/circuits
contains several sample circuit
files
• xor.lsc
: a two input XOR circuit
• mux4.lsc
: a 6 input circuit that behaves like a 4:1
MUX
• inc.lsc
: the next state logic for a 3-bit cyclic
counter
• dec2_4.lsc
: a 2:4 decoder circuit
For example, running the command
./logicSim circuits/xor.lsc
will read that file, add all the components to a circuit, wire them
together, and then attempt to print its truth table to the console. The
same folder contains sample output of my solution run on each of these
circuit files. For example the file circuits/xor.out
is the
result of my solution running on circuits/xor.lsc
.
The code, as it stands, defines a main
in
LogicSim.cc
. That code builds an instance of class
Circuit
from that input file, as defined in the files
Circuit.cc
and Circuit.hh
. A circuit is just
made up of a collection of Gate
objects, a combination of
Input
, And
, Or
, Not
,
and Output
objects, linked together, from the circuit’s
outputs, through the logic gates, back to the circuit’s inputs, using
shared_ptr<Gate>
links. There are also symmetric
weak_ptr<Gate>
links that go from the circuit’s
inputs, through the logic gates, to the circuit’s outputs. The
components are all defined in Gate.cc
and
Gate.hh
.
The sample files do not specify circuits with “cycles”, that is, no
gate’s output eventually feeds its way back into itself as input. They
are all combinatorial circuits because there areno cycles.
Part 4 BONUS Exercise
There is more to tell you about the whole implementation, but first
I’d like to summarize this bonus assignment: You are to complete
one method of the circuit class, namely the method:
• Circuit::evaluate
: This should work through the
components of a circuit, setting the outputs based on the logic
components’ behavior and connectivity, from some set of input
settings.
This code is used by the method
Circuit::outputTruthTable
. It cycles through all the
possible input settings, evaluates the circuit for each, and then
outputs each as a row of a well-formatted truth table.
A lot of code is written for you. The bulk of the your assignment is
actually to read through the existing code, understand it, and use it to
write the code for this evaluate
method in
Circuit.cc
.
Some notes on the
circuits
code
Gates
There are five different component types, each a subclass of
class Gate
. They share the following methods and
attributes:
• name()
: a string identifying the component from the
file
• reset()
: gets the component ready to be evaluated
• evaluate()
: the rule for computing the output of the
component from its input(s).
• _inputs
: wired links to other components that feed into
this component
• _outputs
: wired links to other components that are fed
this component’s output
• _outputValue
: this is the value computed by
evaluate
and is read by the method
outputValue
.
• _outputReady
: indicates whether this component has had
its evaluate
method called yet, since reset
.
This is used by the outputIsReady()
method.
• _numberOfInputsReady
: a count of how many of this
component’s input components have had their evaluate
method
called. This is used by the allInputsAreReady()
method.
• _signalReady()
: method that is called at the end of
evaluate
, alerting subsequent components
Here are the differences amongst the component types:
And
: outputs true
only if all of its input
components are feeding true
. Can have 0 or more inputs fed
to it.
Or
: outputs true
only if at least of its
input components is feeding true
. Can have 0 or more inputs
fed to it.
Not
: should only have one fed input. Inverts that to
produce its output.
Input
: should have no fed inputs. Instead, has a method
setValue
to set its value to true
or
false
.
Output
: should feed its output nowhere and should have
only one fed input.
For all of them, evaluate
computes the component’s rule
for producing its output, sets its _outputValue
, and then
signals that evaluation’s completion by decrementing
_numberOfInputsReady
for each of the components that are
fed by it.
Computing with a Circuit
You are asked to write Circuit::evaluate
, the code for
evaluating all the components of a circuit. Before describing that
method’s algorithm below, first it’s best to understand the steps
incolved in computing with a circuit.
To compute with a Circuit c
, code is expected to follow
these four steps:
Call c.reset()
. This “clears” all the state of each
of the circuit’s components, specifically by having them indicate that
their outputs aren’t “ready”, and thus that none of their fed inputs are
ready either.
Call c.setInputsFrom(ibv)
with some vector
ibv
of type vector<bool>
. It sets each
of the Input
components of c
to
true
or false
based on a
vector<bool>
with inputSize()
elements.
Make sure that ibv.size()
is the same as
c.inputSize()
. This call gets the inputs ready for the next
step.
Call c.evaluate()
. This should simulate the circuit
and ultimately set the outputs. You need to write this
code.
Call c.readOutputsInto(obv)
with some vector
obv
of type vector<bool>
. Make sure its
length is c.outputSize()
. This reads the outputs of the
circuit into that vector.
Your code for c.outputTruthTable
should repeatedly
perform steps 1 through 4, once for each different set of boolean
inputs.
The algorithm for Circuit::evaluate()
There are several ways you can write your code for
evaluate
. All of the ways should, in essence, traverse the
“network” of wired components, determining outputs of the circuit based
on the inputs to the circuit.
Here is a standard description of one such evaluation scheme: keep
track of which components have all their fed inputs “ready”, and haven’t
yet been evaluated. Pick one such gate g
that is ready in
this way, and call g.evaluate()
on it. This, presumably,
will make other components ready. Keep repeating this until all the
components have been evaluated. Note that this process can be started by
choosing any of the Input
components since they have no fed
inputs. Because their _inputs
vector has size 0, then
allInputsAreReady
will be true for them.
Other methods can involve performing a search through the circuit,
either by working forward from the inputs, evaluating those, then
evaluating the components they feed into, etc, until the circuit’s
output values have all been determined.
Another possibility is to make this search a recursive one. You start
with some output, look at what wire feeds into it, and figure out the
output of each such gate. To do that, you can recursively figure out the
output of each of the gates that produce a signal of some wire feeding
into it. Continuing this way, you reach the inputs, terminating that
particular recursive call of the backward search immediately.
NOTE: if you do choose this backwards
recursive method, you must write it in such a way that each
backwards-reachable component of the circuit is evaluated exactly
once.
BONUS Part 5: a Cobra
interpreter
In the folder named cobra
I’ve given a full suite of C++
source files that define tools for executing programs written in the
Cobra programming language. The language is very similar to Python,
shares much of the basic syntax, except it is limited in that it only
computes with integers. It has no other data types, though text strings
can be output. The syntax also does not rely on indentation.
Here is a sample Python program that outputs the positive divisors of
an integer:
def mod(number,divisor):
while number < 0:
number = number + divisor
while divisor <= number:
number = number - divisor
return number
def divides(d,n):
if ((not (d == 0)) and (mod(n,d) == 0)):
return 1
return 0
value = int(input("Enter a number: "))
candidate = 1
while not (value < candidate):
if divides(candidate,value) == 1:
print(candidate)
candidate += 1
The code consists of two function definitions mod
and
divides
followed by a “main” script sitting at the
bottom.
Here is the equivalent Cobra program (also given as a file
samples/divisors.cob
):
def mod(number,divisor):
begin
while number < 0:
number = number + divisor
while divisor <= number:
number = number - divisor
return number
end
def divides(d,n):
begin
if ((not (d == 0)) and (mod(n,d) == 0)):
return 1
return 0
end
begin
value = input("Enter a number: ")
candidate = 1
while not (value < candidate):
begin
if divides(candidate,value) == 1:
print(candidate)
candidate += 1
end
end
The key difference you’ll see is the explicit way that blocks of
code, several lines long, need to be marked with a begin
followed with an end
. This is needed for function bodies
that are a sequence of more than one program statement (the
divides
functionhas two statements: an
ifand a
return). It is also needed for
if-elseor
whilebodies that are sequences of more than one statement (the
whileloop in main is two statements: an
ifand an increment; the main script has three statements: an input assignment, an initialization assignment, and a
while`
loop). This is because Cobra, unlike, Python, does not rely on
indentation (for better and for worse).
There is a lot of code provided in this folder. The
Makefile
builds to different executables. One of these
cobrac
is actually used for the sixth part of this
homework. We’ll talk about this later when we describe BONUS
Part 6. The Makefile
also describes the
construction of an executable named cobra
. This is the
Cobra interpreter. If you compile it using make
, you can
then run Cobra programs, including the ones in the samples
folder. For example, you can run
make cobra
./cobra samples/hello.cob
and it should build the Cobra interpreter, and then run it on the
Cobra version of the Hello, world!
program.
The bulk of the files provided in this folder are related to the
parsing of Cobra source files (ending in .cob
) and building
a syntax tree data structure that represents the function definitions
and main script of the Cobra program it processes. These are essentially
the same as the work you did for Part 3 of this homework. The syntax
tree hierarchy is defined in cobra-ast.hh
and consists of
subclasses of these classes:
• class Prgm
: a Cobra program; contains the syntax trees
for each function definition • class Defn
: the syntax tree
for a function definition’s code.
• class Stmt
: the syntax tree for every possible statement
allowed in Cobra.
• class Expn
: the syntax tree for every possible arithmetic
expression allowed in Cobra.
• class Cndn
: the syntax tree for boolean expressions used
in Cobra if
and while
.
These each rely heavily on std::shared_ptr
to represent
the structure of a parsed Cobra program’s code.
To run a Cobra program, the cobra
executable essentially
traverses the syntax tree of the main script code, executing its program
statements line by line. When a function is called (either as a
procedure or to compute a value), that function’s definition is looked
up, and its syntax tree is then traversed with a set of passed
parameters. Cobra statements can modify the variables of the main
script, or of a running function. Cobra arithmetic and condition
expressions can look up the values of those variables. All this work is
handled by these major functions:
• Prgm::run
: run the main script of a Cobra
program.
• Defn::call
: execute a Cobra function definition with some
passed parameters.
• Stmt::exec
: execute a Cobra statement, possibly modifying
(local) variables.
• Expn::eval
: evaluate a Cobra arithmetic expression,
yielding an integer value.
• Cndn:chck
: check whether a Cobra boolean condition is
true
or false
.
Take a look at these methods for each subclass in
cobra-ast.cc
.
Part 5 BONUS Exercises
Exercise 5.1: if
Write the code for IfTn::exec
which executes
if
statements that don’t contain an else:
. You
can mimic what was written for IfTE::exec
for statements
if
statements that have an else:
.
Exercise 5.2: +=
Write the code for Updt::exec
which executes statements
that update a variable by incrementing them by an expression. For
example, the Cobra statement
x += i*i
is an example of an Updt
term that adds the square of
i
to the variable x
.
Exercise 5.3: -
Write the code for Mnus::eval
which evaluates subtraction
expressions like
y - 35
You can look at my code for Plus::eval
and
Tmes::eval
which handle addition and multiplication.
Exercise 5.4: <=
Write the code for LEql::chck
which checks comparison
conditions like
y - 35 <= 2 * x
You can look at my code for Less::chck
and
Eqls::chck
which handle “less than” and equality
checking.
Exercise 5.5: or
Write the code for OrCn::chck
which checks distunctions of
conditions like
(y < 35) or (y == 35)
You can look at my code for AndC::chck
which handles
conjunctions.
Exercise 5.6: for
Write ForL::exec
to execute Cobra for
loop
statements with the syntax
for <name> = <start> to <end>: <body>
Here name
is a variable name, start
and
end
are integer expressions, and body
is a
statment (or sequence of statements) that gets executed once each time
the for
loop counts upward. For example the Cobra
program
begin
sum = 0
for count = 1 to 10:
begin
print("count")
sum = sum + count
end
print(sum)
end
will print the text count
on 10 lines, followed by the
value 55
.
BONUS Part 6: a Cobra
compiler
The folder named cobra
also contains C++ source code for
building a compiler cobrac
. This program takes
Cobra programs (like hello.cob
) and produces their
equivalent in MIPS assembly that you can run in SPIM. If you type these
commands
make cobra
./cobrac samples/hello.cob
./spim -f samples/hello.s
then, assuming you’ve put spim
and its support files in
this folder, this will compile and run a MIPS program named
hello.s
built from the Cobra source code
hello.cob
.
The compiler relies on the same parsing infrastructure used by the
interpreter cobra
in Part 5. But, rather than calling
Prgm::run
on the parsed Cobra program, it instead runs
Program::compile
to produce the appropriate .s
MIPS assembly file. The compiler makes several passes over the source
code:
• PASS #0: read and parse the Cobra source program,
converting its function definitions and main script into a collection of
syntax trees describing their code. This is the same as what is done by
cobra
in Part 5 of this homework.
• PASS #1: convert the statements and expressions of
the syntax tree of the Cobra program into sequences of
“pseudo-instructions.” These pseudo-instructions are subclasses of the
class INST
defined in cobra-assem.hh
. They are
almost exactly MIPS assembly instructions except, rather than acting
directly on MIPS processor registers, they instead act on temporary
variables. This allows functions to have essentially an infinite
supply of storage values to operate on (rather than on a limited number
of registers).
• PASS #2: read through the sequences of
pseudo-instructions for each Cobra function definition, assigning each
temporary variable a slot in a stack frame for that function.
• PASS #3: read again through the sequences of
pseudo-instructions, writing them out as a series of MIPS assembly
instructions. These snippets of MIPS code will load registers with
values taken from the stack frame, act on those registers to modify a
register, then store that result back into the stack frame.
The code for PASS #0 is complicated. It sits
primarily in cobra-scanner.cc
and
cobra-parser.cc
. These are not written directly by me.
Rather they are constructed from cobra-scanner.ll
and
cobra-parser.yy
using the Unix compilation tools
Flex and Bison.
The code for PASS #1 is distributed throughout the
code of cobra-assem.cc
as the assemble
methods
of subclasses of Stmt
, the assembleToSet
methods of subclasses of Expn
, and as the dual
assembleToBranchIfTrue
and
assembleToBranchIfFalse
methods of subclasses of
Cndn
. These are what you’ll be asked to extend in the
exercises below.
The code for PASS #2 is the method
Assembly::frameVars
that traverses the assembled
pseudo-instructions of a Cobra program. It relies on each
INST
subclass defining a frameVar
for its
pseudo-instruction.
Finally, the code for PASS #3 is distributed
throughout cobra-assem.cc
by defining a toMIPS
method for each of the pseudo-instructions that are subtypes of
INST
.
Part 6 BONUS Exercises
Exercise 6.1-6.6 Write the appropriate “assemble”
methods for each of the Cobra syntactic constructs listed as exercises
in Part 5. This involves performing the work of PASS #1
for each of them. You’ll generate pseudo-instructions (objects who are a
subtype of INST
) for each of them, appending them to the
given Assembly
object.