- Design Specification
- Stubbed Implementation
- Expanding Stubs
- Final Implementation (Not!)
- Main
Stubbed Implementation
I'll follow the top-down implementation strategy, meaning that I'll run the program first and implement it later. (Actually, I'll run it as soon as I create stubs for all the top-level classes.)
Let's start with the Scanner. It's constructed out of a buffer of text (a line of text, to be precise). The Scanner keeps a pointer to that buffer, and later it will be able to scan the buffer left to right and convert it to tokens. For now, the constructor of the Scanner stub announces its existence to the world and prints the contents of the buffer.
class Scanner { public: Scanner (char const * buf); private: char const * const _buf; }; Scanner::Scanner (char const * buf) : _buf (buf) { std::cout << "Scanner with \"" << buf << "\"" << std::endl; }
NOTE
Notice that in order to include a double quote (") in a string literal, it has to be preceded by the escape characterthe backslash (\). The backslash itself won't become part of the string; it's there only to tell the compiler that the following quote is not the end of the string. If you really want a literal backslash in your string, you have to double it.
The SymbolTable stub will be really trivial for now. We'll assume only that it has a constructor.
class SymbolTable { public: SymbolTable () {} };
The Parser will need access to the scanner and to the symbol table. It will parse the tokens retrieved from the scanner and evaluate the resulting tree. The method Eval is supposed to do that and return a status code that depends on the result of the parsing. We combine the three possible statuses into an enumeration. An enum is an integral type that can take only a few predefined values. These values are given symbolic names and are initialized to concrete values either by the programmer or by the compiler. In our case, we don't really care what values correspond to the various statuses, so we leave it to the compiler. Using an enum rather than an int for the return type of Eval has the advantage of stricter type-checking. It also prevents us from returning anything other than one of the three values defined by the enum.
enum Status { stOk, stQuit, stError }; class Parser { public: Parser (Scanner & scanner, SymbolTable & symTab); ~Parser (); Status Eval (); private: Scanner & _scanner; SymbolTable & _symTab; }; Parser::Parser (Scanner & scanner, SymbolTable & symTab) : _scanner (scanner),_symTab (symTab) { std::cout << "Parser created\n"; } Parser::~Parser () { std::cout << "Destroying parser\n"; } Status Parser::Eval () { std::cout << "Parser eval\n"; return stQuit; }
Finally, here's the main procedure. You can see the top-level design of the program in action. The lifetime of the SymbolTable has to be equal to that of the whole program, since it has to remember the names of all the variables introduced by the user during one session. But the scanner and the parser can be created every time a line of text is entered. The parser doesn't have any state that has to be preserved from one line of text to another. If the parser encounters a new variable name, it will store the name in the symbol table that has a longer lifespan.
const int maxBuf = 100; int main () { char buf [maxBuf]; Status status; SymbolTable symTab; do { std::cout << "> "; // prompt std::cin.getline (buf, maxBuf); Scanner scanner (buf); Parser parser (scanner, symTab); status = parser.Eval (); } while (status != stQuit); }
In the main loop of our program, a line of text is retrieved from the standard input using the getline method of cin. This is a standard way of extracting a whole line of text, as opposed to extracting individual words or numbers using >>. A scanner is constructed from the line, and a parser is created from the scanner. The Eval method of the parser is then called to parse and evaluate the expression. As long as the status returned by Eval is different from stQuit, the whole process is repeated. The do/while loop introduced here differs from the while loop in that its body is always executed at least once.
This program compiles and runs, thus proving the validity of the concept.