Как реализовать LOOP в FORTH-подобном интерпретаторе языка, написанном на C

Я пишу простой язык на основе стека на C, и мне было интересно, как мне реализовать какую-то структуру цикла и/или символы просмотра вперед. Поскольку код для этой страницы немного длинный (более 200 строк), я поместил его в репозиторий GitHub.

РЕДАКТИРОВАТЬ: Основная программа находится в файле stack.c.

РЕДАКТИРОВАТЬ: код просто принимает ввод в words, вроде FORTH. Он использует scanf и работает слева направо. Затем он использует серию ifs и strcmps, чтобы решить, что делать. Это действительно так.


person tekknolagi    schedule 04.08.2011    source источник
comment
Небольшой совет: если код слишком велик, чтобы публиковать его здесь, вероятно, он слишком велик для того, чтобы мы могли его изучить, чтобы помочь вам. Разбейте проблему на что-то меньшее и опубликуйте это здесь.   -  person paxdiablo    schedule 05.08.2011
comment
Любые идеи, как я мог бы сформулировать меньшую проблему, принимая во внимание мой существующий код?   -  person tekknolagi    schedule 05.08.2011
comment
@paxdiablo: на самом деле это простой многопоточный интерпретатор. Код большой, но не сложный - сама проблема на самом деле уже достаточно мала.   -  person Jeremy W. Sherman    schedule 05.08.2011


Ответы (4)


Подход Forth заключается в добавлении отдельного стека циклов рядом со стеком данных. Затем вы определяете операции, которые работают с этим стеком циклов. Например:

5 0 DO I . LOOP

Будет печатать

0 1 2 3 4

Как это работает:

  • DO перемещает индекс (0) и элемент управления (5) в стек цикла.
  • I копирует верхнюю часть стека цикла в стек данных.
  • LOOP увеличивает индекс (верхняя часть стека циклов). Если индекс меньше контрольного (на единицу ниже вершины стека цикла), то он повторно запускает команды из DO обратно в LOOP. Если индекс >=, то он извлекает индекс и управление из стека цикла, и управление возобновляется как обычно.
person Jeremy W. Sherman    schedule 04.08.2011
comment
Вот это да. Спасибо за это понимание! Это точно будет полезно! - person tekknolagi; 05.08.2011
comment
Есть предложения по реализации на моем языке? - person tekknolagi; 05.08.2011
comment
Вы можете просто добавить static struct loop { double index; double control; size_t buffer_size; char buffer[]; } loopStack;. Затем eval нужно будет использовать буфер для сохранения всего после DO, пока он не увидит LOOP, чтобы он мог воспроизвести его, как указано в index и control. - person Jeremy W. Sherman; 05.08.2011
comment
что означает static? и какое значение имеет loop? (оба в определении структуры) - person tekknolagi; 05.08.2011
comment
static говорит хранить переменную в статическом хранилище. loop — идентификатор структуры; это позволяет объявлять другие переменные типа struct loop loop2;. - person Jeremy W. Sherman; 05.08.2011
comment
Действительно ли в Форте есть стек циклов? Я всегда думал, что он использует стек возврата для циклов. - person Andriy M; 05.08.2011
comment
В Forth есть стек данных и стек возврата. Форт обычно использует стек возврата для хранения счетчика циклов и лимита. Поскольку в коде OP нет стека возврата, ему он нужен, и его также можно назвать стеком цикла. - person Rudy Velthuis; 05.08.2011
comment
Должен ли I быть I? - person tekknolagi; 05.08.2011
comment
Это I в Форте по уважительной идиоматической причине (и с братьями и сестрами J и K для вложенных циклов), но опять же: вы не реализуете ничего отдаленно похожего на Форт, поэтому не имеет значения, как вы называете свой индекс цикла. - person Julian Fondren; 05.08.2011
comment
То есть буква остается неизменной? Фух. - person tekknolagi; 06.08.2011
comment
Как насчет вложенных операций DO/LOOP? - person pmarreck; 09.06.2017
comment
@pmarreck Re: вложение: вот почему это цикл стек. - person Jeremy W. Sherman; 09.06.2017
comment
Но затем он повторно запускает команды из DO обратно в LOOP, что означает, что он где-то отслеживает все эти команды (например, в другом стеке?), чтобы знать, как их повторно выполнить, нет? Поскольку стек данных потребляется по ходу? Или он хранит некоторую информацию о режиме, например, в середине обработки конструкции DO... LOOP где-то (конечно, в стеке)? - person pmarreck; 09.06.2017
comment
Неважно, я понял это (так что вложенные DO-LOOPs работают!). Просто нужно было вставлять ссылку на текущий стек команд в стек цикла всякий раз, когда встречается DO. Переосмыслил это. Я писал Forth runner на Elixir в основном в качестве упражнения, это действительно легко написать на этом функциональном языке :) github.com/pmarreck/elixir-snippets/blob/master/ (конечно, вместе со встроенным набором модульных тестов) - person pmarreck; 09.06.2017
comment
@pmarreck повторно запускает команды из DO обратно в LOOP. Команды хранятся где-то кроме стеков (данные и стек возврата/цикла)? Можно ли перезапустить только с двумя стеками? - person sinoTrinity; 24.04.2019

Очевидный способ добавить управляющие структуры в язык, основанный на стеке, — это добавить «стек управления». Я опишу механизм Postscript, потому что знаю его, но Форт ведет себя аналогично (конечно, с небольшими отличиями).

Самый простой управляемый цикл — это цикл repeat. Технически бесконечный loop проще, но для его использования требуется явная команда exit, и ее объяснение усложнит ситуацию.

Синтаксис повтора:

int proc  **repeat**  -

Поэтому, когда начинается команда repeat, она ожидает найти процедуру на вершине стека операндов (это тело цикла, которое нужно выполнить) и целое число непосредственно под процедурой (количество раз для выполнения процедуры).

Поскольку также существует стек выполнения (я думаю, Форт называет его стеком «возврата»), команда может «выполнять» другие команды, помещая их в стек выполнения, таким образом планируя выполнение других команд сразу после завершения текущей команды. , но перед возобновлением вызова текущей команды. Это длинное предложение, но ключевая мысль там.

В качестве конкретного примера предположим, что интерпретатор выполняется из входного потока. И стеки выглядят так:

operand: -empty-
execute: <stdin>

Пользователь вводит 2 {(Hello World!\n) print} repeat.

Целое число 2 помещается в стек.

operand: 2
execute: <stdin>

Тело процедуры в кавычках (*) помещается в стек.

operand: 2 {(Hello World!\n) print}
execute: <stdin>

Команда repeat выполняется.

operand: 2 {(Hello World!\n) print}
execute: <stdin> repeat

Repeat: expects operands: int proc
    if int<0 Error
    if int==0 return //do nothing
    push `repeat` itself on exec stack
    push quoted proc on exec stack
    push int-1 on exec stack
    push "executable" proc on exec stack

Выполнение процедуры повтора (один раз) оставляет стеки такими:

operand: -empty-
execute: <stdin> repeat {(Hello World!\n) print} 1 **{(Hello World!\n) print}**

Интерпретатор выполняет процедуру поверх стека exec, печатая «Hello World!», затем находит целое число и помещает его в стек op.

operand: 1
execute: <stdin> repeat {(Hello World!\n) print}

Интерпретатор выполняет процедуру в кавычках, помещая ее в стек операций.

operand: 1 {(Hello World!\n) print}
execute: <stdin> repeat

И мы вернулись к началу! Готов к следующей итерации (или завершению, если целое число упало до нуля).

Надеюсь это поможет.

Изменить;

Посмотрев на ваш код, у меня есть другое предложение, возможно, трамплин к чему-то вроде того, что я описал выше. Я думаю, вам следует на время забыть о коде и сосредоточиться на структурах данных. У вас есть хорошая таблица для переменных, но все команды — это встроенные литералы, разбросанные по коду!

Если ваша таблица содержит переменные типы записей, вы можете использовать один и тот же механизм поиска для обоих (и даже перейти к хэшу или троичному дереву поиска (мой текущий фаворит)). Я имею в виду что-то вроде этого:

struct object {
    int type;
    union {
        int i;
        void (*command)(context *);
    } u;
};

struct dict {
    int sz;
    struct rec {
        char *key;
        object val;
    }  data[]; //think resizable!
};

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

Тогда вам понадобится другой тип для хранения последовательностей команд. Массив или список должны работать. Когда вы можете определить последовательности, вам будет намного легче повторять последовательности.

Бонус в том, что вы всего в одной конструкции от Turing Complete. Последовательности, итерации, решения (или выборка): это все, что вам нужно для программирования любой вычисляемой функции!


*. Как обнаружил комментатор, Postscript на самом деле не имеет той же концепции цитирования, которую я использую здесь в своем описании. Здесь я заимствую эту концепцию из терминологии Лиспа. Вместо этого Postscript имеет флаг literal/executable, который можно установить cvx cvlit или проверить xcheck. Будет выполнен исполняемый массив в стеке выполнения. Таким образом, тело процедуры "в кавычках" фактически является буквальным телом процедуры (т. е. массивом). Из-за этого repeat также должен отправить вызов cvx для выполнения перед следующей итерацией. Итак, более правильный псевдокод для постскриптумной реализации repeat:

Repeat: expects operands: int proc
    if int<0 Error
    if int==0 return //do nothing
    push `repeat` itself on exec stack
    push 'cvx' on the exec stack
    push cvlit(proc) on exec stack
    push int-1 on exec stack
    push "executable" proc on exec stack

При этом выполняется необходимая перестановка флагов для передачи процедуры как данных в стек выполнения.

Я видел еще один способ реализации этой управляющей структуры с двумя функциями, repeat одной и той же точкой входа и внутренним оператором продолжения, который теоретически не нуждается в имени. Я думаю, что ghostscript называет это чем-то вроде @repeat-continue@. С отдельной функцией continue вам не нужно так тщательно следить за порядком элементов в стеке, и вам не нужно менять флаг literal. Вместо этого вы можете хранить некоторые постоянные данные ниже рекурсивного вызова в стеке exec; но его тоже надо убирать.

Таким образом, альтернативным repeat будет:

int proc  **repeat**  -
    if int<0 Error
    if int==0 return //do nothing
    push null on exec stack   <--- this marks our "frame"
    push int-1 on exec stack
    push proc on exec stack
    push '@repeat-continue' on exec stack
    push executable proc on exec stack

Продолжение также имеет более простую работу.

@repeat-continue
    peek proc from exec stack
    peek int from exec stack
    if int==0 clear-to-null and return
    push '@repeat-continue' on exec stack
    push executable proc on exec stack

Наконец, оператор exit тесно связан с циклами, он очищает стек exec до «кадра» оператора цикла. Для стиля с двумя функциями это объект null или аналогичный. Для стиля с 1 функцией это сам оператор цикла.

person luser droog    schedule 05.08.2011
comment
поместите цитируемый proc в стек exec. Я ничего не могу найти в цитируемых процедурах. Зачем это нужно цитировать? - person juFo; 13.05.2013
comment
Под в кавычках я подразумеваю буквальную версию процедуры. Процедура поверх стека exec будет выполнена, но одна из них должна быть литеральной, чтобы она просто помещалась в стек операндов для следующей итерации. - person luser droog; 13.05.2013
comment
@juFo Я добавил больше деталей постскриптума. Извините за путаницу. :) - person luser droog; 22.11.2014

Ваш язык совсем не похож на Forth, поэтому не копируйте структуры циклов Forth (только для компиляции - бессмысленное различие для вашего языка).

Глядя на свой код, добавьте until в качестве условного слова оценки перезапуска. То есть добавьте его как обычное слово рядом с вашими range и jump, сделайте так, чтобы оно вытолкнуло стек, и, если вершина стека была истинной, установите cmd stack.c обратно в начало.

0
dup . 1 + dup 5 > until
.

, на вашем языке, выдаст результат 0 1 2 3 4 5 6 для трех вычислений и повторного вычисления второй строки несколько раз. Это предполагает, что вы сохраняете состояние между оценками, и что оценка ориентирована на строку. Вы можете найти на LSE64 другие подобные идеи.

person Julian Fondren    schedule 05.08.2011

Вот сообщение в блоге, в котором DO/LOOP, BEGIN/UNTIL, WHILE/REPEAT и т. д. реализованы в моем маленьком проекте TransForth: http://blogs.msdn.com/b/ashleyf/archive/2011/02/06/loopty-do-i-loop.aspx

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

Надеюсь, это поможет, получайте удовольствие!

person AshleyF    schedule 09.08.2012