I've been trying to improve my C++ skills, and deiced to try my hand at making a interpreter for a toy language. The language is called Quartz, and so far the only thing you can do is output strings. The following keywords can be used to print out a string: output
, which prints all the strings on one line, and nl_output
, which prints each string on a different line.
The following program is valid in Quartz:
nl_output "Hello World"
nl_output "Goodbye Wolrd"
nl_output "This is a test of the Quartz language"
Each file has the .qz extension, and is basically like a text file.
The over-view of how my interpreter works is:
- It first opens a .qz, and then checks if the file was opened successfully.
- After ensuring that the files has been opened properly, the file contents is then read into a string. The string is then feed to a lexer that checks for the tokens. The lexer use a
for
-loop to iterate over the string, and adds any tokens it finds to a vector. - The lexer then returns the vector to be read by the parser. The parser uses a while loop to iterate over the vector, and calls the correct code if a keyword/keywords is found.
main.cpp
#include<iostream>
using std::cout;
using std::cerr;
using std::endl;
#include<fstream>
using std::ifstream;
using std::fstream;
#include<string>
using std::string;
using std::getline;
#include<vector>
using std::vector;
void open_file(const char *filename, ifstream &data)
{
data.open(filename);
if(data.fail())
{
cerr << "FileError: specified file '" << filename << "' could not be found" << endl;
}
}
vector<string> lexer(string &data_str, ifstream &data)
{
string tok;
string string_var;
string expr;
vector<string> tokens;
getline(data, data_str, '\0');
bool is_string = false;
data_str += '$';
for(unsigned int i=0; i < data_str.length(); i++)
{
tok += data_str[i];
if(tok[tok.size()-1] == '\n' or tok[tok.size()-1] == '$')
{
tok = "";
}
if(data_str[i] == ' ')
{
if(is_string == false)
{
tok = "";
}
else if(is_string == true)
{
tok = " ";
}
}
if(tok == "nl_output")
{
tokens.push_back("nl_output");
tok = "";
}
if(tok == "output")
{
tokens.push_back("output");
tok = "";
}
if(data_str[i] == '"')
{
if(is_string == false)
{
is_string = true;
}
else if (is_string == true)
{
tokens.push_back("string:" + string_var);
string_var = "";
is_string = false;
tok = "";
}
}
if(is_string)
{
string_var += tok;
tok = "";
}
}
/*for(int i=0;i<tokens.size();i++)
{
cout << tokens[i] << ' ';
}*/
//cout << tokens[0] + " " + tokens[1].substr(0,6) << endl;
return tokens;
}
void parser(const vector<string> &tokens)
{
unsigned int i = 0;
while(i < tokens.size())
{
if(tokens[i] + " " + tokens[i+1].substr(0,6) == "output string")
{
if(tokens[i+1].substr(0,6) == "string")
cout << tokens[i+1].substr(8, tokens[i+ 1].size());
i+=2;
}
else if(tokens[i] + " " + tokens[i+1].substr(0,6) == "nl_output string")
{
if(tokens[i+1].substr(0,6) == "string")
cout << tokens[i+1].substr(8, tokens[i+ 1].size()) << endl;
i+=2;
}
}
}
int main(int argc, char *argv[])
{
ifstream data;
open_file(argv[1], data);
string data_str;
vector<string> tokens = lexer(data_str, data);
parser(tokens);
return 0;
}
To test the interpreter, simply compile the code in your command prompt/terminal window.
In my case I did:
g++ C:\main.cpp -o quartz.exe
Then run the [insert executable name].exe
. The .exe takes one command line argument, which is the path to your .qz file. To make the .qz file, make a text file, and choose to rename the extension .qz. Or if you don't want to go through that hassle, a .txt file works fine to.
The three main questions I have are:
- Is the way I'm reading over my string, and adding my tokens to the
tokens
vector inefficient and slow? - Is it bad practice to read A file until a NULL character (
'\0'
) is reached? - Is it mandatory to close a file after opening it? What might occur if I choose not to?