Простой классический способ организовать эти задачи - настроить различные части как подпрограммы, передающие структуры данных друг другу. Прости мой небрежный синтаксис.
Сначала вам понадобится определение токена, произведенного лексером. Это почти всегда структура с перечислением, указывающим, какой тип токена, и тип объединения для переноса любого значения, которое может иметь тип токена:
struct Token {
enum // distinguishes token types
{ EndOfFile, Integer, String, Float, Identifier
Semicolon, Colon, Plus, Minus, Times, Divide, LefParen, RightParen,
KeywordBegin, KeywordDeclare, KeywordEnd, ...
} tokentype
union {
long numeric_value; // holds numeric value of integer-valued token
char* string_value; // holds ptr to string body of identifiers or string literals
float float_value; // holds numeric value of floating-point valued token
} tokenvalue
}
Вы захотите построить абстрактное синтаксическое дерево. Для этого вам понадобится тип TreeNode.
Как и токены, это почти всегда перечисление, указывающее, какой тип узла, и объединение для хранения различных свойств типа узла, и, наконец, список указателей на дочерние элементы:
struct TreeNode {
enum // distiguishes tree node types
{ Goal, Function, StatementList, Statement, LeftHandSide, Expression,
Add, Subtract, Times, Divide, Identifier, FunctionCall, ...
} nodetype
children* TreeNode[4]; // a small constant here is almost always enough
union // hold values specific to node type, often includes a copy of lexer union
{ long numeric_value; // holds:
// numeric value of integer-valued token
// index of built-in function number
// actual number of children if it varies
// ...
char* string_value; // holds ptr to string body of identifiers or string literals
float float_value; // holds numeric value of floating-point valued token
} nodevalue
}
MyCompiler.c содержит следующий код:
int main() {
filehandle Handle = OpenSourceFile(&FileName);
ASTrootnode TreeNode = Parser(Handle);
CodeGenerator(ASTrootnode);
exit();
}
Parser.c содержит следующий код:
TreeNode Parser(filehandle Handle) {
<parsingmachinery>
nexttoken Token=Lexer(filehandle);
<moreparsingmachinery to build tree nodes>
return toplevel_TreeNode;
}
Lexer.c содержит следующий код:
Token Lexer(filehandle Handle) {
token Token;
<process characters in buffer>
if bufferp=end_of_buffer
fileread(filehandle,&bufferbody,bufferlength);
<process characters in buffer>
token.tokentype=<typeofrecognizedtoken>
token.tokenvalue.long=<valueofnumerictoken>
...
return Token;
}
Очевидно, что вы захотите поместить объявления Token и TreeNode в заголовочные файлы, которые можно будет использовать в исходных файлах вашего компилятора.
Если вы создадите высокопроизводительный компилятор, вы захотите оптимизировать эти процедуры. Один тривиальный пример: FileHandle может стать глобальной переменной, и, таким образом, нет необходимости передавать его между частями в качестве явного аргумента. Один не такой тривиальный пример: вам понадобится высокопроизводительный генератор лексеров или кодирование лексера вручную, чтобы максимизировать скорость обработки символов, особенно при пропуске пробелов и комментариев.
Если вы хотите увидеть конкретные подробности о том, как создать синтаксический анализатор, который создает AST, см. Мой SO-ответ по созданию синтаксических анализаторов с рекурсивным спуском: https://stackoverflow.com/a/2336769/120163