Implementation Details

	This document describes the implementation details of the Verilog
Behavior Simulation project.  We will describe the methods in which we
implemented the simulation and the parsing.  The whole design is divided
into two major parts, the parsing and the simulation.  If we take a closer
look at each part, it consists of the following subparts:

	1.  Token scanner
	2.  Grammar parser
	3.  Simulation setup
	4.  Simulation utilities

We will describe each subpart and how they are implemented.  If you wish
to find out why some of these tools were implemented as they were, please
see the implementation decision document. 


Simulator Subparts Overview

	A simulator can be a very large and complex program.  Therefore,
building a scanner in C or C++ by hand can be very complicated and time
consuming.  Therefore the scanner in the design is written in lex and kept
in the file named vbs.l.  The scanner will first read in the input and
then look for specific patterns of interest and divide them into different
named tokens.  These tokens will be used as the inputs to the grammar
parser.  Lex handles some of the complex situations in a very natural way
and make it very easy to write a lex scanner. 

	Again, keeping in mind the time constraint, the grammar parser is
written in yacc rather than C or C++.  The simplicity of yacc has
significantly decrease the size of the program in length.  Another
advantage of yacc is that it is very natural and logical.  The complete
parser codes are stored in vbs.y

	Let's look at how the scanner and the parser are coordinated in
the whole scanning and parsing process.  When they are used together, the
parser is the higher level routine which will call the scanner to detect
the desired patterns (tokens) and return the tokens when detected.  The
main task of the parser is to take the sequences of tokens returned by
scanner and do the syntax and grammar checking.  Both the scanner and the
parser had to agree on these token codes before hand.  These codes are
defined in the yacc parser as it is processed into the vbs_yacc.h (which
is generated by yacc) and in the intrface.h header files.  After the
syntax and grammar checking, if there are no errors, the tokens will be
used to decide which objects to create or used as is.  We are not
implementing error recovery, thus the program will quit parsing a file if
an error is detected in the Verilog description. 

	After the parsing, the next step is to setup the program
simulations.  There are two things to do in the setup:

	1.  Store variable names in the symbol table.
	2.  Store operations (statements) in the time wheel.

When the parser finds a register declaration, the register names and sizes
are stored in the symbol table.  When a function or task definition is
found, they too are stored in the symbol table.  Operations detected in
the Verilog description are put into the time wheel.  The operations are
stored sorted in time, with the operations in the same time being stored
in a link list.  When an event trigger is found, the statement which is to
be executed is placed in the symbol table in the same entry as the
specific register name. 

	Now we are ready to start the simulations.  Simulation will start
once the input file is parsed and the setup are done.  All the functions
that are named Sim_trigger in the Trigger.cc are used to do the
simulations.  What they do is trigger the operations in the time wheel and
delete the operations once they are executed.  In the case of a change in
a variable, the statements that monitor the variable are then added to the
event queue.  At the end of the time unit, this event queue is then
traversed and all statements found will be executed.   This concludes the 
overview.  Now we'll go into detail, starting with the parsing.


Algorithm Details

Reading the Input file(s):

	Lex is the tool that we used to build our lexical analyzer or the
lexer.  In the lexer, a set of lexical specifications is written so that
the lexer can use it to match against the input stream.  The lex (flex) 
program will generate a function called 'yylex()'.  This function is 
called by the yacc parser each time a new token is needed.  YYlex() scans 
in the input stream and tries to match regular expressions.  When it 
finds a recognized expression, the expression is tokenized and returned 
to the parser.

	In lex, regular expressions are used to describe the lexical
specifications.  A regular expression is described in a "meta" language
which specifies an expression pattern of interest and assign a name to it. 
Let's take one of the regular expressions in the vbs.l and see how it is
defined.  For instance, a 'digit' is defined to be [0-9] which means that
a digit can be any number ranging from 0 to 9.  It means the same thing as
[0123456789].  For the very specific keywords that we are interested in,
we just spell them out exactly and lex will tokenize them accordingly. 
Take the word 'begin', for instance, only the input stream that has the
same spelling and all the letters are lower cases will there be a match. 

	Now that we know how to write a lex specification, let's see how a
natural property of a regular expression in lex can save enormous time on
writing a scanner.  Take another example, the definition of the 'Decimal'
in the vbs.l.  We defined it as
   
Digit		[0-9]
DigitU		[0-9_]
Number		{Digit}{DigitU}*
Decimal		{{Number}}?'[dD]{Number}

The 'Decimal' definition specifies that, it consists of zero or one
occurance of 'Number' followed by a single quote "'", then a case
insensitive 'd', and one string representing a 'Number'.  The whole
definition can be defined in four lines (which includes the definitions of
other required regular expressions).  In this case, if there are any
errors, it is easy to check since there are only four lines to look at. 
Imagine if this was implemented in C or C++, the length could double and
checking it will not be easy. 

	Now we have a scanner to read the input stream and separate the
stream into tokens.  The lex program will generate a C function called
'yylex()' which will be called by our grammar parser.  The grammar parser
is written using yacc.  A yacc description looks like the following: 

assignment:	lvalue '=' expression
			{ /* C function to create objects */ }
	;

lvalue:		identifier
			{ " " }
	;

expression:
		identifier
			{ " " }
	|	expression '+' expression
			{ " " }
	|       expression '-' expression
                        { " " }
 	;

The symbol on the right side of the colon represents an object.  Each
definition specifies what this object consists of.  The "assignment" 
object consists of a "lvalue" and an equal sign followed by an expression.
The "lvalue" object consists of an "identifier", where an identifier is
just a word in the input stream.  The "expression" object consists of
multiple definitions.  Each definition is separated by a symbol '|'.
Recursion occur where the definition includes the name of the object, such
as the definition for the add expression.  At the end of all definitions
for an object, we end the definition with the semicolon. 

	When the yacc parser finds all the tokens that make up an object,
the C functions in the braces are called to create the object.  However,
because the lexer and yacc parser are both in the C language, and our
objects are built using the C++ language (functions in PTypes.cc), a
conflict exists.  Thus a function is needed to act as an interface to the
functions in PTypes.cc and the parser.  Functions in the intrface.c are C
functions called by the parser.  These functions then allocate storage 
for the C++ objects and call the C++ function to create those objects and 
store them into the allocated space.

	Object such as "expressions" and "lvalues" are created when they
are found.  Once these objects are created, they are passed to another
function to create the larger objects, namely "assignment".  This process
of creating larger objects from smaller objects continue until a complete
description of a circuit is parsed.  The object that results from this
complete description is called a 'module' object.  Once we have this
'module' object, we can begin the setup and simulation. 


Simulation Objects

	Before we describe the setup and simulation procedure, let's take
a look at the objects needed by the setup and simulation functions.  These
objects consists of the following: 

	1.  Symbol table
	2.  Time wheel
	3.  Event queue

Along with these objects, other utility objects were also needed.  In
keeping with the concept of objects in C++, anything that can be
structures in C are made into objects in C++.  These include: 

	1.  Bool
	2.  String
	3.  Bitvector
	4.  List
	5.  Error
	6.  Event
	7.  Parser objects
	8.  Task and system task object

Let's describe these utility objects before discussing the larger objects. 
We will describe them as listed above. 

	The file Bool.h contains the definition of the 'Bool' type.  It is
simply an enumerated type.  String.h contains another basic class.  our
string implementation is heavily based on the GNU C++ library.  But we
simplified the class so to reduce compile time and errors.  We chose to
implement our own bool and string type even though ANSI C++ draft already
defined one.  We did this because not many C++ compiler support this new
standard yet.  It should not be difficult to modify the source code to use
the ANSI standard.  Simply defining "String" to "string" should work. 

	The 'Bitvector' class is our representation of a register.  See
the implementation decisions for a discussion on why we used this
representation.  Bitvector is also based on the String class of GNU C++
library.  A 'SubBitVector' is used to represent a range selection.  For
the SubBitVector class, only assignment is required.  This is because all
evaluation of expressions are done on 'Numbers' (see parser objects). 
Thus, we never have to operate on SubBitvectors.  BitVector contains the
following information :
	
	1.  Begin/End index
	2.  Size
	3.  MSB location
	4.  Array of bits

Because we want to use memcpy and memcmp for array manipulation, we chose
to use 8 bit storage for the 'Bits' type.  Since enumeration types take 32
bits, we typedef'ed Bits to unsigned char.  As a result, we also had to
define the four bit types; HI, LO, DC (don't care), and Z (high
impedance).  Because of the way Verilog declare registers, we also keep
track of the starting and ending index positions.  This is because the
starting position does not have to be zero.  The size is also
precalculated, so future reference does not need recalculations.  The most
signification bit location in Verilog register declaration is also not
fixed.  I.e. the following are valid register declarations:

	reg [0:7] regA;
	reg [7:0] regB;

Because of this, we have to keep a status flag to indicate which of these
was used to declare the register.  With these information, our bit vector
object is able to do all basic logic arithmetic, such as shifting,
addition, inversion, etc.  The implementation of these operations are done
according to the Verilog Language Reference Manual (LRM) [OVI92].  Please
see the LRM (or source code) for details. 

	The 'List' class is in List.h.  This class represents all lists in
our project. Thus, it is a template class.  The list is implemented as a
first in first out (FIFO) link list.  Included is a size of the list, so
that we can use the list like an array.  This allows us to overload the
"[]"  operator to allow for index referencing.  A function called 'Size'
is also available to determine the size of the array.  A stack object was 
also created during the upgrade process.  A stack was needed to keep 
track of the scope of declarations.  Stack is also kept in List.h.

	Since all C structures could be C++ objects, we made error
handling an object.  This allows us to add new error messages easily.  The
error handling routines are in Error.cc.  The error handling object
consists of three things; array of error messages, enumerated list of
error numbers, and the Sim_perror function call.  There is one global
variable which needs to be set at startup, the program name.  The
variables that need to be set at run-time are; Sim_errno, for the type of
error; Sim_lineno, if the line number is known; and the Sim_filename, if
more than one file is used.  To add new error detection, you need to do
two things.  1.  Define a symbol for the error message in Error.h (Errno). 
2.  Add a message to the 'Sim_errmsg' in Error.cc.  Now you are done.  The
only thing to do now is to detect the error and set the run-time variables
mentioned above. 

	The event object is used for event control in the 'always @'
construct.  An event object consists of an EvntExpression and a list of
event expressions called 'OredEventExpression'.  The event expression
object contains a member to specify which type of event trigger is
monitored.  The supported types are edge trigger (posedge and negedge) and
level trigger.  The event object also has a pointer to the statement to be
executed when the variable is modified.  An additional data member called
'bit' was added to support detection of scalar event expressions.  I.e.
for edge triggered, event expressions must be scalar.  To detect this, we
used this 'bit' data member to hold the actual bit used.  If the variable
is only one bit, then 'bit' is zero.  Otherwise, it is set accordingly, by
the setup procedure. 

	The parser objects are created by the parser, but is used by the
simulation.  Thus I will use the term simulation objects and parser
objects interchangibly.  Parser objects consists of any objects that are
parsed in the Verilog description.  Examples include lvalues, expressions,
statements, etc.  Large objects like statements consists of smaller
objects like lvalue and expressions or delay/event controls. 

To create a new parser object, you need to decide two things;  1.  what
type of object is it (statements, expressions, or independent).  2.  what
makes up this object (expressions, statements, etc).  These two things
will tell you which member functions are needed.  The following is a list 
of member functions and their purpose:

1.  Sim_setup -- is used to do all the setup for simulation.  This 
includes things like calculating the index into the symbol table, or
creating new symbol table nodes for IO and net declarations. 

2.  Sim_trigger -- is used to execute a statement.  It also takes care of
handling other things like executing the system task '$write'. 

3.  Sim_evaluate -- is used to evaluate an expression.  Thus only
expression types require to have this function.  All Sim_evaluate
functions return a 'Number' for the result.  Thus, all operations are
performed on 'Number' objects.  But Number just encapsulates the
'BitVector' object. 

4.  copy -- is the virtual copy constructor for derived classes.  These 
include all statements, expressions and module items.

5.  display -- is a debug function which outputs the contents of the 
object to standard output.  All objects have this function.

Data members are determined by the type of object, which can be found in 
the "Formal Syntax Definition" section of the Verilog LRM.  For example, 
the function object is defined as:

<function>
	::= function <range_or_type>? <name_of_function> ;
		<tf_declaration>+
		<statement>
	    endfunction

I skipped the <instance_attribute>, but basically this is the definition. 
Thus, a function parser object would need to contain a range, a rangeid, a
list of tf_declarations, and a statement.  Two other data members were
also added to the parser object types to support error reporting.  They
are filename (filename) and line number (lineno).  They are inherited from
the base class and set by the functions in PTypes.cc.  If a class is not a
derived class, then these two membe data objects are part of the class
itself. 

Creating a new parser object also requires you to write an interface
function.  An interface function is a function that sets up the structures
for the C++ function to create the actual objects.  All interface
functions are in intrface.c.  These functions allocate a structure to
store the C++ object (a pointer to void).  This structure is then passed
back and forth in the parser code.  When a larger object is created,
'statement' from 'lvalue' and 'expression', the smaller objects (lvalue
and expressions) is freed.  Once these two things are done, you can now
add the grammar parser into the vbs.y file and parse the new Verilog
construct. 

	Originally we seperated the system functions and tasks.  Then when
we wanted to support function definition, we were required to move the
'FunctionCall' parser object from PTypes.h into the same file as our
system functions and tasks.  Both system functions and tasks and user
defined functions and tasks use the same object.  This was not a design
decision, but rather a compromise during coding.  It was simply
inefficient to seperate system functions and user defined functions.  They
both behaved exactly the same.  Thus, writing two of them was not good use
of time. 

Functions and tasks are implemented in Systask.cc.  These parser objects 
contain the same data as before.  But now, instead of calling fixed 
statements and expressions, we look into the symbol table for the 
expression or statement.  Thus, functions and tasks have the following data:

	1.  Index (into symbol table) of function or task.
		(This required that system functions and tasks be
		stored in the symbol table with the rest of the
		symbols.)
	2.  List of arguments.

See below on how functions and tasks are triggered.  This concludes the
description of all the basic data types of the simulator.  The rest of the
data types are objects that use these data types.  We'll start with the
symbol table. 


Symbol Table

	The symbol table is implemented as an array.  Each entry in the
symbol table consists of the following information:  (words in parenthesis
are variable names in the structure)

	1.  Availability indicator (taken)
	2.  Symbol type indicator (type)
	3.  Name of symbol (name)
	4.  List of statements for monitoring (stlst)
	5.  Pointer to data value (value)

While this was ok for a single module, it did not support scope rules. The
new symbol table implementation uses a list of the above nodes to keep
track of the scope levels.  The symbol table consists of an array of lists
(which most people call buckets).  The list contains a type called
'STnode', which contains many of the above data information.  One data
type not needed is the 'Taken' data member.  Since each bucket is a list
that can grow, we will not find any "taken" buckets.  The original idea
used STnode as a base class in C++.  The derived classes consisted of
registers, functions, tasks and modules.  Each class contains different
information.  But to add support for IO declarations for functions, it was
not possible to use polymorphism in this way.  Thus the new implementation
has all the information in the STnode class.  The implementation 
decisions document discusses the reasons in more detail.

The best way to describe the symbol table is to describe how it adds new
symbols. 

The answer lies in the use of the following object.

class HashValue
	{
	unsigned long value;
	int scope;
	};

The 'value' data is used as the index into the array.  'Value' contains
the hashed number of the string.  The hash function used can be found in
[Holub90] (this implementation is also modeled after one of the symbol
tables described in this book).  The 'scope' is a key number used with
each scope level.  The scope is incremented everytime a new scope is
found.  I.e.  every time a new module is instantiated, the scope is
incremented.  The global scope number is kept in the symbol table itself
so that it can be easily accessed. 

Once we have the hash value, we use the 'value' data to obtain the index
into the array.  This gives us a list of 'STnodes'.  We then search this
list for a node with the same 'scope' as our hash value.  If none is
found, we can add this symbol into the table.  If another node with the
same hash value is found, we can have either a collision or a duplicate
symbol.  On a collision, we rehash the symbol and repeat the above steps. 
We simply quit when a duplicate is found.  Lookup is done with the same
exact steps.

The current implementation required that all information for all 
different data types must be stored in STnode (no more inheritance).  
This required the following list of data members for STnode.

	1.  Status flag for complete node (complete).
	2.  Type (type).
	3.  Direction (dir).
	4.  Range of storage (if any) (ms, ls).
	5.  Name of symbol (name).
	7.  Pointer to storage for anything that needs it (storage).
	8.  Pointer to list of statements (monitors).
		(For monitoring this symbol).
	9.  List of module items (moditems).
	10.  List of hash values for arguments to function call (iovars).
	11.  Pointer to expression for functions (expr).
	12.  Pointer to statement for functions and tasks (stmt).
	13.  Index into port connection symbol (portidx).
	14.  List of indices to propagate signals (propagate).

Verilog has the register declaration and IO direction on different
statements.  Thus, we must set these status at different points in the
program.  This required all storage be available whether they are used or
not.  For example, tasks do not need storage for numbers, or a pointer to
an expression.  Thus, these pointers are never used for this type of data. 
See below on what needs to be done to setup the symbol table.


Time Wheel

	The time wheel is maintained in a two dimensional link list.  The
operations are sorted in time and are stored in the order they are
received.  Operations that have the same accumulated time delay will be
linked at the same time node.  Each time node is also a link list.  This
link list appends operations in the order that they are received.  This
means if two operations are to be executed at the same time, the one that
was found first will be at the head of the list and executed first.  This
is to assure that all the operations that have the same accumulated time
delay will be triggered at the "same" time by the Trigger function.  Once
an operation is triggered , it will be deleted from the linked list, this
goes on until the last operation in the node is triggered.  The node with
no operations in the list will be deleted from the timewheel.  When there
are no more time nodes, then simulation is complete. 


Event Queue

	The event queue is similar to the time wheel, in that it keeps
track of all the statements that needs to be executed.  The reason why we
have an event queue is because some events must be executed at the end of
a time unit.  This means we can not append the statement to the end of the
current time node.  As a result, we need a seperate list of statements to
be executed after the current time.  The event queue consists of a link
list (List object) of statements.  The list object contains all the
operations we need for the event queue, such as adding a new statement and
getting the first statement.  The event queue does not need any other
operations.  Now that we know how all the objects in this program are
created.  We can discuss how the simulation procedure works.  We start
with the setup. 


Simulation procedure


Setup

	After all the objects are parsed and created but before the
simulation begins, object setup takes place.  Here I will discuss how
objects are set up and which error checking takes place.  All setup
functions are kept in the file called Setup.cc.  Note, since now we
support scope rules, the scope number is past into each setup function so
that it can be used in the hashing function.  The scope is kept in a stack
with static objects.  This allows us to check for the closest scope, which
is at the top of the stack. 

	The set up procedure starts from the function Sim_start in
Setup.cc.  It sets up the simulation environment.  Setup starts from the
largest object which is called 'module'.  Module contains all other
objects called module items and they are to be setup in the hierachical
order.  To setup the object 'module', we loop through the entire list of
module items and call the setup function for each of the module items
found.  These setup functions will then call the corresponding setup
functions to do the set up for individual objects.  This goes on and on,
until we get to the object which has no smaller objects to setup.  Then we
are finished.  Next, I would like to discuss some of the set up functions
in Setup.cc that needs more detailed discussion.  The following is a list
of other setup that needs to be done: 

	1.  Register and IO declarations
	2.  Event expression
	3.  Module Items with delay
	4.  Function and task definition.
	5.  Module instantiation.


Register and IO declarations

	GetRangeData is called to get the range of the register
declaration.  It returns the left number and the right number of the range
in the register declaration.  Example: 

	reg [3:0] a, b, c;

In this case the left number is 3 and the right number is 0.  Next, 'ids'
is a list of registers (a, b, and c) and 'range' ([3:0]) contains the
ranges, that is three and zero in this case.  A loop is used to step
through each variable name.  First we check whether this variable name is
already declared.  This is done by making a function called to 'Lookup' in
the symbol table.  If it doesn't exist, we allocate a new 'STnode' object
and add it to the symbol table with 'NewSymbol'.  Otherwise, we must check
the type of the existing variable name.  This is done by calling the
'SetType' of the node. 

SetType is a member function of 'STnode'.  It tests the node for valid
data and sets the type accordingly.  For instance, if we are declaring a
register, we must make sure the current symbol is a wire with the IO
direction of 'output'.  Otherwise, we can not make this symbol a register
type (registers cannot be used as inputs).  After the type is set, we must
add data storage to objects that need it.  For instance, in register
declarations, we must add a number to the 'storage' field.  We do this by
calling 'SetStorage' in the node.  'SetStorage' will check to make sure
the node type is valid for this operation.  If not, an error is displayed. 
Then we check to see if this node is complete.  By complete, we mean if
this node has all the data set correctly.  If so, we set the 'complete'
flag to true.  Otherwise, we wait and let the setup function check whether
we are complete or not.  This same type of operation is done for the
input, output, and inout declarations. 


Event Expressions

	The first thing in the event expression setup is to get the index
in the symbol table.  There are three types of trigger types that we care
about, they are:  positive edge, negative edge and value change.  The
trigger type of the event is determined and the setup of each event
expression is done according the the type obtained.  If the trigger type
is edge change, i.e. positive edge or negative edge, then a check is done
to make sure the data is scalar.  If the data is not a scalar data, the
program will generate an error message and exit.  Otherwise, the event
expression is appended to the list in the symbol table to be used during
simulation time. 


Ored Event Expressions

	The whole list of or'ed event expression is step through and set 
up.  For example:

	always @(negedge a or b or c)
		statement_or_null;

The always statement will be setup for all the event expressions, negedge
a, b and c.  In this case setup for event expression is called up three
times to set up the statements. 


Module Items with Delays

	Only the always statement is concerned here.  In the 'always'
statement if there is a delay then setup for the delay needs to be done. 
The delay can be a number or a range id.  For a number type delay, a new
timewheel node is created and appended to the timewheel according to the
delay.  If the delay type is a rangeid, the index to the range id has to
be obtained first.  With the index obtained, the data stored in the symbol
table is extracted and a timewheel node is created and appended to the
timewheel.  Note, this rangeid is not a register type, since register data
types are not yet initialized at this point.  Thus, rangeid in this
context are parameter definitions in the Verilog description. 


Function definition

	When a function definition is found, we must add it to the symbol
table so when a function call is used, it will find this symbol in the
symbol table.  Thus, the first thing we do is try to add a new symbol into
the symbol table.  Once this is done, we must step through the list of
required declarations.  Register and IO declarations are done like the
above.  The setup function also adds a list of input, output and inout
declarations to the function.  When all setup is done, we check this list
to make sure that all IO declarations are complete.  For example; 

function [7:0] name_of_function;
	input x;
	output y;
	inout z;
	begin
	end
endfunction

In this function definition, we must make sure that 'output y' must have a
register attached to it.  Without it, we can not assign a value to 'y',
because it has no storage space.  As a result, the above definition would
trigger an error.  A task definition is setup in the same way.  The only 
difference between a function and a task setup is that tasks does not 
have storage for the return value.  Otherwise, task setup is exactly the 
same.

	Module instantiation setup require some extra effort.  When a 
module instantiation is found, we call the setup for each instance.  The 
instance will then create a new symbol table node.  Then the module being 
instantiated is retrieved from the symbol table.  The module items in 
this module are then setup using a new scope.  Setting up a new instance 
needs to do the following things:

	1.  Create new symbol table node for each port in the module.
	2.  Attach the port connections from the module instance.
	3.  Add the new node index into the port connection's propagate
		list.  This assures that we get a signal when something
		in the instantiating module changes.
	4.  Setup all the module items in this module.

The symbol table is created in such a way that when a variable changes in
the parent module, the instantiated module is sent a signal so it can do
its work.  This concludes the setup procedure.  Now we can begin the
simulation. 


Simulation

	Once the setup is done, we can start the simulation procedure.  
The simulation procedure consists of the following steps:

	1.  Get the next statement to execute from the time wheel.
	2.  Execute the statement or append to the future.
	3.  If more statements in current time, goto 1, else goto 4.
	4.  Execute all statements in the event queue.
	5.  Goto 1.

We begin by getting the next instruction in the time wheel.  This
statement is usually a sequential block from an 'initial' procedural
block, 'always' procedural blocks usually do not go into the time wheel.
We now execute the statement.  In the case of the sequential block, we
must handle it differently.  Take the following example: 

	initial
		begin
		a = 0;
		b = 0;
		#1 b = 1;
		#3 if (b == 1)
			a = b + 5;
		a = b - 5;
		end

In the above example, we have to determine when to execute all the 
statements.  Previously, we used the following algorithm:

	1.  Is there a delay?
		If so -- append to the future, and mark the delay as used.
	2.  Is there a accumulated delay?
		An accumulated delay is when there were delays in
		previous statements.  This is the case for the
		'a = b - 5' statement.  This statement should be
		executed at time 4.  This requires us to keep track
		of the accumulated delay for each sequential block.
	3.  If there's no delay and there's no accumulated delay,
		then execute this statement.  This is the case for
		'a = 0' and 'b = 0'.  There is no accumulated delay
		and no delay in front of the statement.

With the delay in nested block fix, we now use the following algorithm:

	1.  (Sequential block) Is block in the stack?
		If no -- push this block into the stack.
	2.  Pop nesting level from stack and get next statement.
		No more nesting level:  return 'TRUE' from this execution.
		No more statements:
			a.  If there is no more nesting levels left, set
				this block as not pushed into the stack.
				So, on the next execution, we can do the
				test in 1.
			b.  If nesting level is part of loop(for):
				Return 'TRUE' from this execution.
				(This is done because 'for' loops need to
				update variables and compare expressions.
				If we do not return, this is never done)
			c.  Else goto 3.
	3.  Push statement into stack.
		(Need to do this because of nested blocks)
	4.  Execute statement, passing in the original statement.
	5.  What did statement return?
		FALSE (execution has been delayed):
			a.  If curstmt is NULL, pop something into it.
			b.  Return 'FALSE' from this execution.
		TRUE (finished):
			Pop statement out of stack and goto 3.

For non-sequential block statements, we use the following algorithm:

	1.  Is there a delay?
		a.  If yes -- append original sequential block(*) into time
		wheel, then delete delay from the statement.
		b.  Otherwise (no delay), we execute this statement.
	2.  Return 'TRUE' from this execution.

(*) Original sequential block is the one attached to the 'initial'
procedural block.  It is passed into the trigger function.

This procedure is done for each and every statement we find.  The reason
is because we do not know what statement we have (polymorphism).  Thus,
all statements have the same behavior.  This procedure only works for
'initial' procedural blocks.  For 'always' statements we just execute the
statement, since we do not support delays.  Statements from 'Always'
procedural blocks are appended to the event queue when a register is
modified. 

When an assignment statement is triggered, we first get the node from the
symbol table for the variable being modified.  Then we call the member
function 'Assignment' of 'STnode' to do the actual modification of the
variable.  Then a check is made to see if this variable was monitored by
any 'always' statement.  This is done by testing the 'monitors' data
member for statements.  If it is non-null, there are statements to be
appended to the event queue.  For example; 

	always @(posedge varA)
		statement_1

	always @(varA)
		statement_2

In this case, 'statement_1' and 'statement_2' should be in the 'monitors'
list.  Now we determine which type of change was monitored by this
statement. This is done using the 'trigger' data member in the 'Event'
object.  It tells us the monitor type; edge change or level change (change
in value).  We call the member function 'QueueEvnt' and pass it the type
of change that happened.  If there was no change, we go to the next
monitor event.  This continues until there are no more monitor events
found in the 'monitors' list.

When a function or task is executed, we need to prepare the local 
variables.  We call a function named entry_iovars() to save the function 
or task arguments.  This function does not call the Assignment() function 
of the local variables, because this is not an actual assignment.  We 
just want to update local variables so that the statement to be executed 
will have the correct data.  We also call a similar function once this 
statement has been executed.  The function named exit_iovars() is called 
to update the variables with output or inout direction flags set.

Simulation time is also when many error checking is done.  All errors
listed in Error.h are checked during simulation and setup.  Though some of
them should be checked during setup rather than simulation time.  The
simulation can end in two ways;  1.  No more statements are found in the
time wheel and the event queue.  When this happens, we just return to the
main and quit.  2.  If the '$finish' system task is called.  If '$finish'
is called (executed), we do not care if there are more statements in the
time wheel or event queue, we simply printout a message and exit the
simulator.  This concluded the simulation procedure and the simulation
program. 


Conclusion

	The purpose of this project was to create a functional simulator 
for the Verilog HDL.  For this reason, we tried to simplify the project 
so we do not need to implement unnecessary modules.  Thus, we used the 
lex and yacc tools for the parser.  To reduce the time for the 
development, we also chose to use C++ for it's object oriented features.  
Using C++ simplified the creation of the objects for the simulation 
procedure.
