BASIC Interpreter, Part VI

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 DoubleExpressions 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 DoubleExpressions, 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 preExecute() function to our Program class. Here is the new signature in the header file, program.h:

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.

2 thoughts on “BASIC Interpreter, Part VI

  1. Pingback: BASIC Interpreter, Part V | Visceral Logic Programming

  2. Pingback: Basic Interpreter, Part VII | Visceral Logic Programming

Leave a Reply

Your email address will not be published. Required fields are marked *