Как развернуть (скомпилировать) цикл интерпретатора?
Я слышал, что некоторые языки переходят от интерпретируемых к компиляции путем "разворачивания цикла интерпретатора".
Скажем, у меня есть следующий интерпретатор псевдокода для дерева аш.
int interpret(node)
{
switch(node) {
case PLUS:
return interpret(child(0))+interpret(child(1));
case MINUS:
return interpret(child(0))-interpret(child(1));
}
}
Как мне развернуть этот цикл для создания скомпилированной программы?
Я вижу, что вы все делаете это, как будто я не знаю, о чем говорю, но вот цитата из Википедии, в которой говорится, что именно я описываю.
"Фактор первоначально интерпретировался, но теперь полностью скомпилирован (не оптимизирующий компилятор в основном разворачивает цикл интерпретатора"
Ответы
Ответ 1
"Развертывание цикла" обычно означает замену повторения последовательностью действий. Цикл:
for (int i = 0; i < 4; ++i) {
a[i] = b[i] + c[i];
}
будет разворачиваться в эквивалент:
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
a[2] = b[2] + c[2];
a[3] = b[3] + c[3];
Мне кажется, что тот, кто цитировался в Википедии, использовал эту фразу в несколько метафорическом смысле. Итак, в этом смысле...
Ваш образец обычно вызывается внутри интерпретатора, который идет по дереву узлов AST, который может выглядеть примерно так:
ASSIGN
|
+--+---+
| |
REF MINUS
| |
x +--+---+
| |
VAR PLUS
| |
a +--+--+
| |
VAR CONST
| |
b 3
и функция interpret
имела бы дополнительные опции:
int interpret(node) {
switch(node) {
case PLUS:
return interpret(child(0))+interpret(child(1));
case MINUS:
return interpret(child(0))-interpret(child(1));
case ASSIGN:
return set(child(0), interpret(child(1));
case VAR:
return fetch(child(0));
case CONST:
return value(child(0));
...
}
}
Если вы идете в АСТ с помощью этой функции interpet
(фактически выполняете операции), вы интерпретируете. Но если функция записывает действия, которые необходимо выполнить, а не выполнять их, вы компилируете. В псевдокоде (на самом деле, псевдокод дважды, поскольку я предполагаю, что гипотетическая машина стека является целью компиляции):
string compile(node) {
switch(node) {
case PLUS:
return(compile(child(0))) + compile(child(1)) + ADD);
case MINUS:
return(compile(child(0))) + compile(child(1)) + SUB);
case ASSIGN:
return(PUSHA(child(0))) + compile(child(1)) + STORE);
case REF:
return(PUSHA(child(0)));
case VAR:
return(PUSHA(child(0)) + FETCH);
case CONST:
return(PUSHLIT + value(child(0)));
...
}
}
Вызов compile
на то, что AST (игнорируя любые опечатки псевдокода;-), выплевывал бы что-то вроде:
PUSHA x
PUSHA a
FETCH
PUSHA b
FETCH
PUSHLIT 3
ADD
SUB
STORE
FWIW, я бы подумал об этом как о разворачивании АСТ, а не о разворачивании переводчика, но не буду критиковать чужую метафору, не читая ее в контексте.
Ответ 2
Я немного смущен. Я не думаю, что "разворачивание цикла" - правильный термин здесь. Даже если вы реорганизуете код, чтобы не иметь никаких рекурсивных вызовов, вы все равно будете использовать интерпретатор.
Вы можете скомпилировать эту программу с помощью GCC. Тогда у вас будет скомпилированная программа, хотя скомпилированная программа будет интерпретировать AST.
Один из способов превратить это в компилятор - вместо того, чтобы делать return interpret(child(0))+interpret(child(1));
, вы будете генерировать инструкции по сборке, которые будут делать добавление вместо этого, а затем вывести их в файл.
Ответ 3
У вас на самом деле нет цикла, так как не все вызовы на interpret
являются хвостовыми вызовами.
Самый близкий к вам компилятор, предполагающий модель стека...
int compile(node)
{
switch(node) {
case PLUS:
return compile(child(0))&&compile(child(1))&&compile_op(op_plus);
case MINUS:
return compile(child(0))&&interpret(child(1))&&compile_op(op_minus);
}
}
но я думаю, что разворачивание в этом контексте более применимо к интерпретатору байтового кода, а не к интерпретатору AST. Инструкции байт-кода обычно интерпретируются в цикле. Затем метод "разворачивания" должен испускать код, соответствующий каждой инструкции байткода.
Фактор аналогичен FORTH. Обычно FORTH имеет внешний интерпретатор, который генерирует многопоточный код. В потоковом коде может быть представлен массив указателей на функции (имеется несколько вариантов: прямая резьба, непрямая резьба, подпрограмма и т.д.). Код с резьбой выполняется внутренним интерпретатором. Развертывание интерпретатора в этом случае относится к внутреннему интерпретатору и заключается в конкатенации потока кода.
Ответ 4
Factory - это язык на основе стека, а не интерпретатор на основе AST.
Я использовал языки на основе стека для интерпретаторов актеров, так вот как я сделал работу, которая не может быть совсем не похожа на Factor.
Каждая функция реализована как функция, которая берет стек и возвращает стек (в моем случае мутированная версия того же стека, я не уверен, что Factor функционирует или мутирует). В моих интерпретаторах каждая функция также помещает продолжение функции в верхнюю часть стека, поэтому интерпретатор знает, что делать дальше:
Таким образом, интерпретатор для вызова следующей функции в стеке выглядит примерно так:
for (;;)
stack = (stack[0].function_pointer)(stack);
Учитывая функцию foo:
def foo (x,y):
print( add(x, y) )
add можно определить как:
pop a
pop b
stack[ return_offset ] = a + b
return stack
и foo as:
pop x
pop y
push _
push &print
push y
push x
push &add
и стек для вызова foo (5,6) будет развиваться примерно так на каждом шаге цикла:
>> foo(5,6)
[&foo, 5, 6]
[&add, 5, 6, &print, _]
[&print, 11]
=> 11
[]
Простой компилятор может развернуть цикл для функции foo, генерируя эквивалентный код с резьбой:
compiled_foo (stack):
stack = begin_foo(stack) // arranges stack for call to add
stack = add(stack)
stack = print(stack)
return stack
Ответ 5
Это может быть не связано, но также проверить вторую проекцию Футамуры
http://en.wikipedia.org/wiki/Futamura_projection
в котором говорится, что компилятор - это просто интерпретатор с частичной оценкой/постоянным сгибанием (хорошо теоретически, но не практикуется).
Ответ 6
В в этой статье Я рассмотрел пример автоматического преобразования интерпретатора в компилятор (хотя и скомпилирован на Scheme, а не на машинный код). Это та же идея, что и другие, предоставленные здесь, но вам может показаться полезным ее автоматизировать.
Ответ 7
Интерпретатор сканирует каждый байт-код (или AST node) во время выполнения и отправляет вызовы функций (обычно используя оператор switch в бесконечном цикле).
Компилятор делает практически то же самое, но во время компиляции. Компилятор сканирует каждый байт-код (или AST node) во время компиляции и испускает код (машинный код или некоторый промежуточный язык более высокого уровня, такой как C), чтобы вызвать соответствующую функцию во время выполнения.
Ответ 8
Я думаю, что это означает, что вместо того, чтобы зацикливаться на операторах и выполнять их, вы перебираете операторы и выводите код интерпретатора, который был бы выполнен.
В основном, происходит то, что код, который будет выполняться в цикле интерпретатора, встраивается в новую функцию. Цикл становится "развернутым", потому что, когда код выполняется, он больше не находится в цикле интерпретатора, это просто линейный путь через сгенерированную функцию.