Я проектирую интерпретатор, который использует рекурсивный спуск, и я дошел до того, что начинаю реализовывать встроенные методы.
Одним из примеров метода, который я реализую, является * Метод 1003 *, который выводит на консоль, точно так же, как метод python print()
и Java System.out.println()
.
Однако до меня дошло, что существует несколько способов реализации эти встроенные методы. Я уверен, что есть еще много, но я определил 2 жизнеспособных пути достижения этого, и я пытаюсь установить sh, какой путь является лучшей практикой. Для контекста ниже представлены различные слои, которые я использую в моем интерпретаторе, который свободно основан на https://www.geeksforgeeks.org/introduction-of-compiler-design/ и других учебных пособиях, с которыми я сталкивался.
- Lexer
- Parser
- Semanti c Анализатор
- Интерпретатор / генератор кода
1. Создание узла AST для каждого отдельного встроенного метода.
Этот метод влечет за собой программирование синтаксического анализатора для генерации узла для каждого отдельного метода. Это означает, что для каждого метода будет существовать уникальный узел. Например:
Анализатор будет искать узел, когда в лексере будет обнаружен токен TPRINT
.
print : TPRINT TLPAREN expr TRPAREN {$$ = new Print($3);}
;
И вот как выглядит класс печати.
class Print : public Node {
public:
virtual VariableValue visit_Semantic(SemanticAnalyzer* analyzer) override;
virtual VariableValue visit_Interpreter(Interpreter* interpreter) override;
Node* value;
Print(Node* cvalue) {
value = cvalue;
}
}
Оттуда я определяю методы visit_Semantic
и visit_interpreter
и посещаю их, используя рекурсию из верхнего узла.
Я могу вспомнить пару преимуществ / недостатков использования этого метода :
Преимущества
- Когда генератор кода обходит дерево и посещает метод
visit_interpreter
узла Print
, он может сразу же выполнить ответ непосредственно, как он запрограммирован в нем. методы посещения.
Недостатки
- Мне придется написать много кода для вставки и копирования. Мне придется создать узел для каждого отдельного метода и определить его грамматику синтаксического анализатора.
2. Создание узла Generi c AST для узла вызова метода, а затем с помощью таблицы поиска определить, какой метод вызывается.
Это включает создание одного узла c узла MethodCall
и грамматики. чтобы определить, был ли вызван метод, с некоторым уникальным идентификатором , например строкой для метода, на который он ссылается. Затем, когда вызывается метод MethodCall
visit_Interpreter
или visit_Semantic
, он ищет в таблице код для выполнения.
methcall : TIDENTIFIER TLPAREN call_params TRPAREN {$$ = new MethodCall($1->c_str(), $3);}
;
MethodCall
Node. Здесь уникальный идентификатор std::string methodName
:
class MethodCall : public Node {
public:
virtual VariableValue visit_Semantic(SemanticAnalyzer* analyzer) override;
virtual VariableValue visit_Interpreter(Interpreter* interpreter) override;
std::string methodName;
ExprList *params;
MethodCall(std::string cmethodName, ExprList *cparams) {
params = cparams;
methodName = cmethodName;
}
};
Преимущества:
- Один универсальный c грамматика / узел для всех вызовов метода. Это делает его более читабельным
Недостатки:
- В какой-то момент уникальный идентификатор
std::string methodName
необходимо сравнить в таблице поиска, чтобы определить ответ на него. Это не так эффективно, как непосредственное программирование в ответ на методы посещения узла.
Какая практика является лучшим способом обработки методов в компиляторе / интерпретаторе? Существуют ли разные методы, которые лучше, или есть какие-то другие недостатки / преимущества, которые мне не хватает?
Я довольно новичок в разработке компиляторов / интерпретаторов, поэтому, пожалуйста, попросите меня уточнить, есть ли у меня некоторая терминология неправильно.