This is a continuation of Part V. In the previous section, we added support for the GOTO
statement. Since we don’t yet have any control logic, that is not very useful, but it does lay the infrastructure for one of the features we will add this time: IF-THEN
. We will also add the READ
and DATA
statements.
Following the BASIC guide we are using, an IF-THEN
statement takes the following form: <LINE> IF <EXPRESSION> <RELATION> <EXPRESSION> THEN <LINE>
. I.e., compare two expressions using some test operator, and if the result is true, jump to the specified line number; otherwise continue to the next line. We have already defined a token for the =
operator for use in the LET
statement, but we will need to add tokens for the remaining comparisons: <
, >
, <=
, >=
, and <>
.
Let’s start by creating a class to handle the IF-THEN
statement. IfThen
will be a subclass of Program
; it will store pointers to two DoubleExpression
s to be evaluated at run time, which comparison is to be performed, and the line number to jump to if the comparison is successful. There are various ways of indicating which operation is to be performed; in this case, I have chosen to store it as a C-style string; using an enum
would be another good option. Here is the header file, ifthen.h:
#ifndef _IFTHEN_H_ #define _IFTHEN_H_ #include "program.h" #include "doubleexpression.h" /* This class provides support for the IF-THEN statement. */ class IfThen : public Program { public: IfThen(DoubleExpression *a, DoubleExpression *b, char *op, int line); ~IfThen(); void execute() const; // run this line of the program void list(std::ostream& os) const; // list this line private: DoubleExpression *a, *b; char *op; int line; }; #endif
The implementation is pretty straightforward. The constructor stores all the member variables, the destructor deletes the DoubleExpression
s, execute()
performs the comparison operation and jumps to the appropriate line if it succeeds, and list()
prints out the statement (which is simplified since we stored the operator as a c-string). So here is ifthen.cpp:
#include <cstring> #include "ifthen.h" #include "basic.h" // create a new statement instance IfThen::IfThen(DoubleExpression *a, DoubleExpression *b, char *op, int line){ this->a = a; this->b = b; this->op = op; this->line = line; } // clean up the expression pointers IfThen::~IfThen(){ delete a; delete b; } // run this line of the program void IfThen::execute() const{ double aVal = a->value(); double bVal = b->value(); bool result = false; if( strcmp(op, "=") == 0 ) result = aVal == bVal; else if( strcmp(op, "<") == 0 ) result = aVal < bVal; else if( strcmp(op, ">") == 0 ) result = aVal > bVal; else if( strcmp(op, "<=") == 0 ) result = aVal <= bVal; else if( strcmp(op, ">=") == 0 ) result = aVal >= bVal; else if( strcmp(op, "<>") == 0 ) result = aVal != bVal; if( result ) Basic::instance()->gotoLine(line); else Program::execute(); } // list this line void IfThen::list(std::ostream& os) const{ os << "IF " << a->list() << ' ' << op << ' '; os << b->list() << " THEN " << line; }
Next, we need to add support in our Bison input file. First, make sure to include the new IfThen
header file:
#include "ifthen.h"
We already have a EQUAL
token defined for the LET
assignment statement, and we will re-use it here since it is the same character. We need to add tokens for IF
, THEN
, and the remaining comparison operators:
%token IF %token THEN %token LESS %token GREATER %token LESSEQUAL %token GREATEREQUAL %token NOTEQUAL
We somehow need to convert these tokens into C-strings to pass to the IfThen
class, so we will create a new non-terminal token comp
that will be an sVal
type to store the operator:
%type <sVal> comp
Create the rule for our new comp
symbol:
comp: EQUAL { $$ = "="; } | LESS { $$ = "<"; } | GREATER { $$ = ">"; } | LESSEQUAL { $$ = "<="; } | GREATEREQUAL { $$ = ">="; } | NOTEQUAL { $$ = "<>"; } ;
And now update the program
rule to recognize IF-THEN
statements:
program: PRINT exprList { $$ = new Print($2); } | LET VAR EQUAL doubleExpr { $$ = new Let($2, $4); free($2); // malloced in basic.l } | GOTO INT { $$ = new Goto($2); } | END { $$ = new End(); } | IF doubleExpr comp doubleExpr THEN INT { $$ = new IfThen($2, $4, $3, $6); } ;
Now, on to the flex input file. Add scanners for the new tokens defined in the Bison input file:
IF { return IF; } THEN { return THEN; } \< { return LESS; } \> { return GREATER; } \<\= { return LESSEQUAL; } \>\= { return GREATEREQUAL; } \<\> { return NOTEQUAL; }
Finally, add the new files in your Makefile. You can now run programs like this:
Welcome to BASIC! Enter a program name: test >10 if 1 < 2 then 30 >20 print "1 > 2" >30 if 1 = 1 then 50 >40 print "1 <> 1" >50 if 2 > 1 then 70 >60 print "2 < 1" >70 if 1 <= 2 then 90 >80 print "1 > 2" >90 if 2 >= 1 then 110 >100 print "2 < 1" >110 if 1 <> 2 then 130 >120 print "1 = 2" >130 end >run >
Adding support for READ
and DATA
will require some reworking of how we do things. Prior to the program running, all the DATA
statements must already be evaluated so their values can be stored and ready for any READ
statement. We will accomplish this by adding a pre-evaluation loop before the main execution loop. But first, we will add storage for the DATA
values and functions to read/write them. We will store the values in a std::deque
, which implements a first-in-first-out queue.
Start by including the header files for std::vector
and std::deque
in basic.h:
#include <map> #include <string> #include <iostream> #include <vector> #include <deque> #include "program.h" #include "doubleexpression.h"
Next, declare functions for the READ
and DATA
statements. read()
will take as input a variable name, and assign to it the next value taken from a DATA
statement. pushData()
will take a vector of doubles, and put them into the data value deque:
public: ... void read(std::string var); // assign next data value to var void pushData(std::vector<double> vals); // push more values onto data vector
Finally, add a private member variable to store the DATA
values:
private: ... std::deque<double> data; // stored data block for READ
Moving on to the implementation in basic.cpp, add the two new functions. read()
will take advantage of our existing assign()
function. pushData()
will iterate through the values in its input vector and push them onto the data
variable:
// assign next data value to var void Basic::read(std::string var){ assign(var, data.front()); data.pop_front(); } // push more values onto data vector void Basic::pushData(std::vector<double> vals){ for( std::vector<double>::iterator it = vals.begin(); it != vals.end(); ++it ){ data.push_back(*it); } }
We also need to modify the execute()
function to add our pre-evaluation loop. This will run through all the Program
instances stored in our lines
map and call preExecute()
, a function which we will add to the Program
class:
// run the program void Basic::execute(){ data.clear(); // clear any existing stored data for( map<int, const Program *>::iterator it = lines.begin(); it!= lines.end(); ++it ){ it->second->preExecute(); } counter = lines.begin(); while( counter != lines.end() ) counter->second->execute(); }
Next, we’ll add the
program.hpreExecute()
function to our
Program
class. Here is the new signature in the header file, :
class Program{ public: virtual void execute() const; // run this line of the program virtual void list(std::ostream& os) const; // list this line virtual void preExecute() const; // run before main program execution };
Since this function is only used by one specialized subclass, the implementation in program.cpp doesn’t do anything:
// nothing to do here... void Program::preExecute() const{ }
Now we will add the Data
class to handle the DATA
statement. Like usual, it will be a subclass of Program
. Unlike usual, it will not override the execute()
function, since it only has activity in the preExecute()
phase. Here is the header file, data.h:
#ifndef _DATA_H_ #define _DATA_H_ #include <vector> #include "program.h" /* This class implements the DATA statement, storing numbers for later use by READ. */ class Data : public Program { public: Data(std::vector<double> vals); // use parent 'execute' implementation void list(std::ostream& os) const; // list this line void preExecute() const; // run before main program execution private: std::vector<double> vals; // doubles to be stored }; #endif
In the implementation, the constructor will take a vector of doubles, and store it in val
. The list()
function will do the normal listing of the statement, and preExecute()
will load the numerical values into the Basic
data storage variable. Note that because list()
is declared const
, we must use a const_iterator
access to the member variable vals
, along with the corresponding cbegin()
and cend()
iterator functions. Here is data.cpp:
#include "data.h" #include "basic.h" Data::Data(std::vector<double> vals){ this->vals = vals; } // list this line void Data::list(std::ostream& os) const{ os << "DATA "; std::vector<double>::const_iterator it = vals.cbegin(); os << *it; // print out first value for( ++it; it != vals.cend(); ++it ){ os << ", " << *it; // print out remaining values } } // run before main program execution void Data::preExecute() const{ Basic::instance()->pushData(vals); }
The Read
class also subclasses Program
. It stores a vector of strings representing variables names to be assigned values from a DATA
statement. Here is the header file, read.h:
#ifndef _READ_H_ #define _READ_H_ #include <vector> #include <string> #include "program.h" /* This class supports the READ statement, putting pre-stored DATA into specified variables. */ class Read : public Program { public: Read(std::vector<std::string> vars); void execute() const; // run this line of the program void list(std::ostream& os) const; // list this line private: std::vector<std::string> vars; // variables names to receive values }; #endif
Here is the implementation, read.cpp:
#include "read.h" #include "basic.h" Read::Read(std::vector<std::string> vars){ this->vars = vars; } // run this line of the program void Read::execute() const{ for( std::vector<std::string>::const_iterator it = vars.cbegin(); it != vars.cend(); ++it ){ Basic::instance()->read(*it); } Program::execute(); } // list this line void Read::list(std::ostream& os) const{ os << "READ "; std::vector::const_iterator it = vars.cbegin(); os << *it; // print out first value for( ++it; it != vars.cend(); ++it ){ os << ", " << *it; // print out remaining values } }
That is all for the C++ classes, so we will now move on to the flex and Bison files. The flex change for this is very simple, we just need to read and return two new tokens that will be defined in the Bison input file:
DATA { return DATA; } READ { return READ; }
In the Bison input file, first include our two new header files:
#include "read.h" #include "data.h"
Next, we need to add a couple new types to the token union definition, one for string lists, and one for double lists:
// token type definition %union { int iVal; double dVal; char *sVal; Program *progVal; Expression *eVal; DoubleExpression *dxVal; std::vector<Expression*> *eList; std::vector<std::string> *sList; std::vector<double> *dList; }
Create two new constant tokens for the new statements:
%token DATA %token READ
Add two new non-terminal symbols for new rules we will create to handle string lists and double lists:
%type <sList> stringList %type <dList> doubleList
Extend the program
rule to handle our new statements, making use of our new constant tokens and non-terminal symbols:
program: ... | READ stringList { $$ = new Read(*$2); } | DATA doubleList { $$ = new Data(*$2); } ;
Finally, we need to add the rules for our new stringList
and doubleList
non-terminal symbols. Similarly to the exprList
rule we already have, the stringList
will consist of one VAR
symbol, optionally followed by any number of COMMA VAR
pairs. The doubleList
actually needs to be able to handle integers as well as floating point numbers, so it will consist of either an INT
or a DOUBLE
token to start with, optionally followed by more INT
and/or DOUBLE
tokens, separated again by commas:
stringList: VAR { $$ = new std::vector(1, $1); } | stringList COMMA VAR { $1->push_back($3); $$ = $1; } ; doubleList: DOUBLE { $$ = new std::vector(1, $1); } | INT { $$ = new std::vector(1, $1); } | doubleList COMMA DOUBLE { $1->push_back($3); $$ = $1; } | doubleList COMMA INT { $1->push_back($3); $$ = $1; } ;
That’s all the code. Now add the new Read
and Data
class files to your Makefile, and build and run. Here is a sample session:
Welcome to BASIC! Enter a program name: readdata >10 data 1 >20 read x, y >40 print x, y >50 data 2 >run 1.000000 2.000000
As always, the complete source files are available here: https://github.com/VisceralLogic/basic/tree/part-6
Continue to Part VII.
Pingback: BASIC Interpreter, Part V | Visceral Logic Programming
Pingback: Basic Interpreter, Part VII | Visceral Logic Programming