This is a continuation of Part IV. In Part V we will add support for parentheses in mathematical expressions, add the GOTO and END statements, and implement the remaining program storage statements: CATALOG, SCRATCH, and RENAME.
Now that we have a framework for supporting numerical operations, adding parentheses is not difficult. An expression wrapped in parentheses is equivalent to a literal double or a variable name, so we will put evaluation into the term rule in Bison. We need a new subclass of DoubleExpression, which we will call ParenExpression. It will store a pointer to an inner DoubleExpression, and return its value when queried. The only thing this new class adds is in the list() function it will print out parentheses so that the program line can be printed out and read back in. Here is the header file, parenexpression.h:
#ifndef _PARENEXPRESSION_H_
#define _PARENEXPRESSION_H_
#include <string>
#include "doubleexpression.h"
/*
This class is used to store a parenthesized expression
*/
class ParenExpression : public DoubleExpression {
public:
ParenExpression(DoubleExpression *exp);
~ParenExpression();
const std::string print() const; // return a printable value
const std::string list() const; // print a listing version
double value() const; // numerical evaluation
private:
DoubleExpression *exp;
};
#endif
Here is the implementation, parenexpression.cpp:
#include "parenexpression.h"
ParenExpression::ParenExpression(DoubleExpression *exp) : DoubleExpression(0){
this->exp = exp;
}
ParenExpression::~ParenExpression(){
delete exp;
}
// return a printable value
const std::string ParenExpression::print() const{
return std::to_string(value());
}
// print a listing version
const std::string ParenExpression::list() const{
return "(" + exp->list() + ")";
}
// numerical evaluation
double ParenExpression::value() const{
return exp->value();
}
In the Bison input file, include the new header file, add two new tokens to represent the parenthesis literals, and modify the term rule to create the new subclass:
#include "parenexpression.h"
...
%token OPENPAREN
%token CLOSEPAREN
...
term:
DOUBLE { $$ = new DoubleExpression($1); }
| VAR {
$$ = new VariableExpression($1);
free($1);
}
| OPENPAREN addExpr CLOSEPAREN { $$ = new ParenExpression($2); }
;
In the flex input file, add scanners for the new parenthesis tokens:
\( { return OPENPAREN; }
\) { return CLOSEPAREN; }
Add the new header and source files to your Makefile, and after compiling you will be able to run programs likes the example below:
Welcome to BASIC! Enter a program name: new >10 print 2*(3+4) >run 14.000000 >20 print 2*(3+4)^2 >run 14.000000 98.000000 >list 10 PRINT 2.000000*(3.000000+4.000000) 20 PRINT 2.000000*(3.000000+4.000000)^2.000000
Now we will add the GOTO and END statements. GOTO is used to jump to an arbitrary line number during program execution, allowing lines to be skipped or executed in a different order. END terminates the current program. Per the BASIC manual we are following, an END statement is required in every program, and it must be the highest line number. We have already deviated from that by not requiring an END statement, and we will further deviate by allowing it to appear anywhere.
To support these new statements, we need to change how we handle program execution. Right now, we have a for loop that steps through every statement in order and executes it. Clearly, this won’t work. What we need to do instead is have a program counter variable that keeps track of which line to execute next, and have functions that allow this to be modified.
Let’s start by updating program.h. Add a new private member variable counter to keep track of which line to execute next. This variable is an iterator of the same signature as used in our current execute() function:
private: ... std::map<int, const Program*>::iterator counter; // program line to run next
Next, add the public member functions to manipulate the counter: one to advance to the next line (standard behavior), one to jump to an arbitrary line, and one to go to the end of the program:
public: ... void gotoLine(int line); // jump to program line void nextLine(); // go to next program line void endProgram(); // end execution
Now we can update the class implementation, basic.cpp. First, update the execute() function to use our new counter variable. Instead of a for loop, we will reset counter to the beginning, and then have a while loop that executes each statement until counter points to the end. Of course, this requires that each statement execution update counter to another line, or we will get stuck execute the same line over and over:
// run the program
void Basic::execute(){
counter = lines.begin();
while( counter != lines.end() )
counter->second->execute();
}
Next, add implementations for the three new functions we declared for manipulating counter:
// jump to program line
void Basic::gotoLine(int line){
counter = lines.find(line);
}
// go to next program line
void Basic::nextLine(){
++counter;
}
// end program execution
void Basic::endProgram(){
counter = lines.end();
}
Now that we have the control logic implemented, we can create the classes that call these function to support the GOTO and END statements. We will create two new subclasses of Program. Goto will take and store a single integer line number; End will take no arguments, and simply call Basic::endProgram() in its execution. Here is the header file goto.h</em:
#ifndef _GOTO_H_
#define _GOTO_H_
#include
#include "program.h"
/*
This class implements the GOTO statement
*/
class Goto : public Program {
public:
Goto(int line); // instantiate goto statement
void execute() const; // run this line of the program
void list(std::ostream& os) const; // list this line
private:
int line; // line to goto
};
#endif
Here is its implementation, goto.cpp:
#include "goto.h"
#include "basic.h"
// instantiate goto statement
Goto::Goto(int line){
this->line = line;
}
// run this line of the program
void Goto::execute() const{
Basic::instance()->gotoLine(line);
}
// list this line
void Goto::list(std::ostream& os) const{
os << "GOTO " << line;
}
The End class is even simpler; all it needs to do is provide implementations of the virtual functions defined by Program. Here is the header file, end.h:
#ifndef _END_H_
#define _END_H_
#include
#include "program.h"
class End : public Program {
public:
void execute() const; // run this line of the program
void list(std::ostream& os) const; // list this line
};
#endif
And the implementation, end.cpp:
#include "end.h"
#include "basic.h"
// run this line of the program
void End::execute() const{
Basic::instance()->endProgram();
}
// list this line
void End::list(std::ostream& os) const{
os << "END";
}
Our new statements will now work, but if we left it like this and tried to run a program, any of the old statements would get stuck in a loop; we need to update them to call Basic::nextLine(). We will call this in Program::execute(), which previously did nothing, and the subclasses will then call Program::execute() after performing their own execution. Here is the new program.cpp:
#include "program.h"
#include "basic.h"
// advance to next line
void Program::execute() const{
Basic::instance()->nextLine();
}
// if you ever see this, something is wrong
void Program::list(std::ostream& os) const{
os << "GENERIC PROGRAM (SHOULDN'T SEE THIS)";
}
Here is the update to Let::execute():
// store the value of the Expression in the Basic vars map
void Let::execute() const{
Basic::instance()->assign(var, expression->value());
Program::execute();
}
And similarly in print.cpp:
// prints out each expression to std::cout
void Print::execute() const{
for( int i = 0; i < exprList->size()-1; i++ ){
cout << exprList->at(i)->print() << ' ';
}
cout << exprList->at(exprList->size()-1)->print() << endl;
Program::execute();
}
Next we will update our Bison input file to use our new statements. First, include the new header files:
#include "goto.h" #include "end.h"
Add two new constant tokens:
%token GOTO %token END
We are going to make some changes to how flex scans for LINE tokens. Right now, it recognizes an integer starting at the beginning of a line as a LINE token. But since the GOTO statement requires a line number to be provided later, we have to recognize an integer anywhere. This will screw up our DOUBLE recognition, which scans for any number not at the beginning of a line, so let’s just rename our LINE token to INT:
%token <iVal> INT
Since we no longer have a LINE token, update the stmt rule to use the new INT token:
stmt:
INT { Basic::instance()->remove($1); }
| INT program { Basic::instance()->add($1, $2); }
| RUN { Basic::instance()->execute(); }
| LIST { Basic::instance()->list(std::cout); }
| NEW { Basic::instance()->newProgram(); }
| OLD { Basic::instance()->oldProgram(); }
| SAVE { Basic::instance()->saveProgram(); }
| UNSAVE { Basic::instance()->unsaveProgram(); }
;
Now add our two new statements to the program rule:
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(); }
;
And since flex will now preferentially return an INT token rather than a DOUBLE, we need to support that in our term rule:
term:
DOUBLE { $$ = new DoubleExpression($1); }
| INT { $$ = new DoubleExpression($1); }
| VAR {
$$ = new VariableExpression($1);
free($1);
}
| OPENPAREN addExpr CLOSEPAREN { $$ = new ParenExpression($2); }
;
In the flex input file, change the LINE scanner to return INT, and remove the caret at the beginning so that it will match an INT anywhere:
{DIGIT}+ { yylval.iVal = atoi(yytext); return INT; }
Also, add scanners for the two new constant tokens:
GOTO { return GOTO; }
END { return END; }
Lastly, add the new header and implementation files to your Makefile. Here is the updated file at this point in time:
.PHONY: all clean all: basic.tab.c lex.yy.c \ basic.h basic.cpp \ program.h program.cpp \ print.h print.cpp \ expression.h expression.cpp \ stringexpression.h stringexpression.cpp \ doubleexpression.h doubleexpression.cpp \ operatorexpression.h operatorexpression.cpp \ let.h let.cpp \ variableexpression.h variableexpression.cpp \ parenexpression.h parenexpression.cpp \ goto.h goto.cpp \ end.h end.cpp g++ -std=c++11 basic.tab.c lex.yy.c program.cpp basic.cpp print.cpp expression.cpp \ stringexpression.cpp doubleexpression.cpp operatorexpression.cpp let.cpp \ variableexpression.cpp parenexpression.cpp goto.cpp end.cpp \ -o basic basic.tab.c: basic.y basic.h expression.h stringexpression.h doubleexpression.h \ operatorexpression.h print.h program.h let.h variableexpression.h parenexpression.h \ goto.h end.h bison -d basic.y basic.tab.h: basic.y bison -d basic.y lex.yy.c: basic.tab.h basic.l flex basic.l clean: rm basic.tab.c basic.tab.h lex.yy.c basic *.o
You can now build and run programs like the following:
Welcome to BASIC! Enter a program name: test >10 print 10 >20 goto 40 >30 end >40 print 40 >50 goto 25 >25 print 25 >list 10 PRINT 10.000000 20 GOTO 40 25 PRINT 25.000000 30 END 40 PRINT 40.000000 50 GOTO 25 >run 10.000000 40.000000 25.000000
OK, the last things we want to add this time are the final program storage statements: CATALOG, SCRATCH, and RENAME. CATALOG lists the saved BASIC files in the current directory; SCRATCH erases the current stored program, but keeps its name; and RENAME keeps the stored program but lets the user enter a new name. This will involve changes to the Basic class, as well as the Bison and flex input files. Let’s start with the Basic header file. We already have a private function erase() that we can repurpose for the SCRATCH functionality, so make that a public function. Also, add two new functions: renameProgram() and catalogPrograms():
class Basic {
public:
...
void saveProgram(); // save active program to disk
void unsaveProgram(); // delete saved program from disk
void newProgram(); // start a new program
void oldProgram(); // load program from disk
void erase(); // clear stored program lines
void renameProgram(); // rename the current program
void catalogPrograms(); // list saved programs
...
For the implementation, our existing newProgram() function contains what we need for our newProgram() function, but first calls erase(). So we will move everything after erase() into the newProgram() and then call newProgram():
// start a new program
void Basic::newProgram(){
erase();
renameProgram();
}
// rename the current program
void Basic::renameProgram(){
std::cout << "Enter a program name: ";
std::cin >> name;
std::cin.ignore(); // consume the newline character
}
Listing the programs that have been saved to disk is a little more complicated. C++ has not included directory access in the standard until very recently. I have used some code that works on Linux and Mac OS X; Windows users will have to find a different solution. First, we need to include a few more headers in basic.cpp:
#include <cstring> #include <set> #include <unistd.h> #include <sys/dir.h>
Now we can add our new catalogPrograms() function:
// list saved programs
void Basic::catalogPrograms(){
char cwd[FILENAME_MAX];
getcwd(cwd, FILENAME_MAX);
DIR* dirp = opendir(cwd);
struct dirent * dp;
std::set set; // store in a set to get alphabetical ordering
while ((dp = readdir(dirp)) != NULL) {
char *sub = strstr(dp->d_name, ".bas");
if( strstr(dp->d_name, ".bas") ){
sub[0] = 0; // end name before .bas
set.insert(dp->d_name);
}
}
closedir(dirp);
// print them out
for( std::set::iterator it = set.begin(); it != set.end(); ++it ){
std::cout << *it << std::endl;
}
}
First we create a character buffer (cwd) that can store the maximum filename length, and populate it with the current directory. Then we open this directory for reading, and step through it, adding every file that ends in “.bas” to a std::set (this automatically sorts them alphabetically, which the directory read may not do). Once we have added them all, we iterate through the std::set and print the results.
Since we haven’t added any new files, our existing Makefile does not need to be changed. The complete source files for this part can be found here: https://github.com/VisceralLogic/basic/tree/part-5
Continue to Part VI.
Pingback: BASIC Interpreter, Part IV | Visceral Logic Programming
Pingback: BASIC Interpreter, Part VI | Visceral Logic Programming