Как развернуть (скомпилировать) цикл интерпретатора?

Я слышал, что некоторые языки переходят от интерпретируемых к компиляции путем "разворачивания цикла интерпретатора".

Скажем, у меня есть следующий интерпретатор псевдокода для дерева аш.

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

Я думаю, что это означает, что вместо того, чтобы зацикливаться на операторах и выполнять их, вы перебираете операторы и выводите код интерпретатора, который был бы выполнен.

В основном, происходит то, что код, который будет выполняться в цикле интерпретатора, встраивается в новую функцию. Цикл становится "развернутым", потому что, когда код выполняется, он больше не находится в цикле интерпретатора, это просто линейный путь через сгенерированную функцию.