Table of Contents

Forth-часть

Теперь мы достигли стадии, на которой работает self-hosted Forth. Все дальнейшие слова могут быть записаны как слова Forth, включая такие слова, как IF, THEN, и.т.д., которые на большинстве языков будут считаться весьма фундаментальными.

Некоторые примечания о коде:

Я использую отступы для отображения структуры. Количество пробелов не имеет никакого значения для Forth, кроме того, кроме того, что вы должны использовать по крайней мере один пробельный символ между словами, а сами слова не могут содержать пробелы. Forth чувствителен к регистру. Используйте CAPS LOCK.

DIVMOD

Примитивное слово /MOD (DIVMOD) оставляет как частное, так и остаток в стеке. (В i386 команда idivl дает оба значения). Теперь мы можем определить / и MOD на основе /MOD и нескольких других примитивов.

: / /MOD SWAP DROP ;
: MOD /MOD DROP ;

Символьные константы

Определим некоторые символьные константы и слова:

  • Перевод строки
  • Пробел
  • Возврат каретки
: '\n' 10 ;       \ Возврат каретки
: BL   32 ;       \ BL (BLank) стандартное слово для пробела

: CR     '\n' EMIT ;  \ CR печатает возврат каретки
: SPACE  BL   EMIT ;  \ SPACE печатает пробел

NEGATE

NEGATE оставляет на стеке обратное число тому, что было на стеке

: NEGATE 0 SWAP - ;

Булевые значения

Стандартные слова для булевых значений

: TRUE  1 ;
: FALSE 0 ;
: NOT   0= ;

LITERAL

LITERAL берет то, что находится в стеке (<foo>) и компилирует как LIT <foo>

: LITERAL IMMEDIATE
    ' LIT ,      \ компилирует LIT
    ,            \ компилирует сам литерал (из стека)
;

Вычисления во время компиляции

Теперь мы можем использовать [ и ] для вставки литералов, которые вычисляются во время компиляции. (Вспомните, что [ и ] являются словами Forth, которые переключаются в и из непосредственного режима.)

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

: ':'
    [         \ входим в immediate mode (временно)
    CHAR :    \ push 58 (ASCII code of ":") в стек параметров
    ]         \ переходим назад в compile mode
    LITERAL   \ компилируем LIT 58 как определения ':' слова
;

Еще несколько символьных констант определим таким же способом.

: ';' [ CHAR ; ] LITERAL ;
: '(' [ CHAR ( ] LITERAL ;
: ')' [ CHAR ) ] LITERAL ;
: '"' [ CHAR " ] LITERAL ;
: 'A' [ CHAR A ] LITERAL ;
: '0' [ CHAR 0 ] LITERAL ;
: '-' [ CHAR - ] LITERAL ;
: '.' [ CHAR . ] LITERAL ;

COMPILE

При компиляции [COMPILE] word компилирует word, в противном случае (при интерпретации) исполняет его "НЕМЕДЛЕННО".

: [COMPILE] IMMEDIATE
    WORD        \ получить следующее слово
    FIND        \ найти его в словаре
    >CFA        \ получить его codeword
    ,           \ и скомпилировать его
;

RECURSE

RECURSE делает рекурсивный вызов текущему слову, которое компилируется.

Обычно, когда слово компилируется, оно помечено как HIDDEN, так что ссылки на одно и то же слово внутри являются вызовами предыдущего определения слова (если таковое есть). Однако у нас все еще есть доступ к слову, которое мы сейчас компилируем с помощью LATEST-указателя, поэтому мы можем использовать его для компиляции рекурсивного вызова.

: RECURSE IMMEDIATE
    LATEST @  \ LATEST указывает на слово, компилируемое в данный момент
    >CFA      \ получаем codeword
    ,         \ компилируем его
;

Управляющие выражения

Пока мы определили только очень простые определения. Прежде чем мы сможем идти дальше, нам нужно сделать некоторые управляющие структуры, например IF ... THEN и LOOP. К счастью, мы можем определить произвольные элементы управления структуры непосредственно в Forth.

Обратите внимание, что структуры управления, так, как я их определил, будут работать только внутри компилируемых слов. Если вы попытаетесь ввести выражения с использованием IF и.т.д. в непосредственном режиме, то они не будут работать. Выполнение этих операций в непосредственном режиме остается в качестве упражнения для читателя.

Условное выражение вида:

condition IF true-part THEN rest

компилируется в:

condition 0BRANCH OFFSET true-part rest

где OFFSET - это смещение rest

А условное выражение вида:

condition IF true-part ELSE false-part THEN

компилируется в:

condition 0BRANCH OFFSET true-part BRANCH OFFSET2 false-part rest

где OFFSET - это смещение false-part и OFFSET2 - это смещение rest.

IF - это НЕМЕДЛЕННОЕ слово, которое компилирует 0BRANCH, за которым следует фиктивное смещение, и помещает адрес 0BRANCH в стек. Позже, когда мы увидим THEN, мы вытолкнем этот адрес из стека, вычислим смещение и заполним смещение.

: IF IMMEDIATE
    ' 0BRANCH ,    \ компилировать 0BRANCH
    HERE @         \ сохранить позицию смещения в стеке
    0 ,            \ компилировать фиктивное смещение
;

: THEN IMMEDIATE
    DUP
    HERE @ SWAP -  \ рассчитать смещение от адреса сохраненного в стек
    SWAP !         \ сохранить смещение в заполненом месте
;

: ELSE IMMEDIATE
    ' BRANCH ,     \ определить ветвь до false-part
    HERE @         \ сохранить местоположение смещения в стеке
        0 ,        \ компилировать фиктивное смещение
        SWAP       \ теперь заполнить оригинальное (IF) смещение
        DUP        \ то же что и для THEN выше
    HERE @ SWAP -
    SWAP !
;

Циклы

Переходим к циклам:

BEGIN - UNTIL

BEGIN loop-part condition UNTIL

компилируется в:

loop-part condition 0BRANCH OFFSET

где OFFSET указатель обратно на loop-part. Это похоже на следующий пример из Си:

do {
    loop-part
} while (condition)
: BEGIN IMMEDIATE
    HERE @       \ Сохранить location в стеке
;

: UNTIL IMMEDIATE
    ' 0BRANCH ,  \ скомпилировать 0BRANCH
    HERE @ -     \ рассчитать смещение от сохраненного адреса в стеке
    ,            \ скомпилировать смещение
;

BEGIN - AGAIN

BEGIN loop-part AGAIN

компилируется в:

loop-part BRANCH OFFSET

где OFFSET указатель обратно на loop-part. Другими словами, бесконечный цикл, который может быть прерван только вызвом EXIT

: AGAIN IMMEDIATE
    ' BRANCH , \ скомпилировать BRANCH
    HERE @ -   \ вычислить смещение назад
    ,          \ скомпилировать смещение
;

BEGIN - WHILE - REPEAT

BEGIN condition WHILE loop-part REPEAT

компилируется в:

condition 0BRANCH OFFSET2 loop-part BRANCH OFFSET

где OFFSET указывает назад на условие (в начало) и OFFSET2 указывает в конец, на позицию после всего фрагмента кода. Это похоже на следующий пример из Си:

while (condition) {
    loop-part
}
: WHILE IMMEDIATE
    ' 0BRANCH ,   \ компилировать 0BRANCH
    HERE @        \ сохранить позицию offset2 в стеке
    0 ,           \ компилировать фиктивное смещение offset2
;

: REPEAT IMMEDIATE
    ' BRANCH ,    \ компилировать BRANCH
    SWAP          \ взять оригинальное смещение (from BEGIN)
    HERE @ - ,    \ и скомпилировать его после BRANCH
    DUP
    HERE @ SWAP - \ вычислить offset2
    SWAP !        \ и заполнить им оригинальную позицию
;

Unless

UNLESS будет таким же как IF, но тест будет наоборот.

Обратите внимание на использование [COMPILE]: Поскольку IF является IMMEDIATE, мы хотим, чтобы он выполнялся, не пока UNLESS компилируется, а пока UNLESS работает (что случается, когда любое слово, использующее UNLESS, компилируется). Поэтому мы используем [COMPILE] для обращения эффекта, который оказывает пометка IF как IMMEDIATE. Этот трюк обычно используется, когда мы хотим написать собственные контрольные слова, без необходимости реализовывать их, опираясь на примитивы 0BRANCH и BRANCH, а вместо этого используем более простые управляющие слова, такие как (в данном случае) IF.

: UNLESS IMMEDIATE
    ' NOT ,        \ скомпилировать NOT (чтобы обратить test)
    [COMPILE] IF   \ продолжить, вызывав обычный IF
;

Комментарии

Forth допускает комментарии вида (...) в определениях функций. Это работает путем вызова IMMEDIATE word (, который просто отбрасывает входные символы до тех пор, пока не попадет на соответствующий ).

: ( IMMEDIATE
    1                  \ разрешены вложенные комментарии путем отслеживания глубины
    BEGIN
        KEY            \ прочесть следующий симво
        DUP '(' = IF   \ открывающая скобка?
            DROP       \ drop ее
            1+         \ увеличить глубину
        ELSE
            ')' = IF   \ закрывающая скобка?
                1-     \ уменьшить глубину
            THEN
        THEN
    DUP 0= UNTIL       \ продолжать пока не достигнем нулевой глубины
    DROP               \ drop счетчик
;

Стековая нотация

В стиле Forth мы также можем использовать (... -- ...), чтобы показать эффекты, которые имеет слово в стеке параметров. Например:

  • ( n -- ) означает, что слово потребляет какое-то целое число (n) параметров из стека.
  • ( b a -- c ) означает, что слово использует два целых числа (a и b, где a находится на вершине стека) и возвращает одно целое число (c).
  • (–) означает, что слово не влияет на стек

Некоторые более сложные примеры стека, показывающие нотацию стека:

: NIP ( x y -- y ) SWAP DROP ;

: TUCK ( x y -- y x y ) SWAP OVER ;

: PICK ( x_u ... x_1 x_0 u -- x_u ... x_1 x_0 x_u )
    1+                  \ добавить единицу из-за "u" в стек
    4 *                 \ умножить на размер слова
    DSP@ +              \ добавить к указателю стека
    @                   \ и взять
;

\ C помощью циклов мы можем теперь написать SPACES, который записывает N пробелов в stdout
: SPACES                ( n -- )
    BEGIN
        DUP 0>          \ пока n > 0
    WHILE
            SPACE       \ напечатать пробел
            1-          \ повторять с уменьшением пока не 0
    REPEAT
    DROP                \ сбросить счетчик со стека
;

\ Стандартные слова для манипуляции BASE )
: DECIMAL ( -- ) 10 BASE ! ;
: HEX     ( -- ) 16 BASE ! ;

Печать чисел

Стандартное слово Forth . (DOT) очень важно. Он снимает число с вершины стека и печатает его. Однако сначала я собираюсь реализовать некоторые слова Forth более низкого уровня:

  • U.R ( u width – ) печатает беззнаковое число, дополненное определенной шириной
  • U. ( u – ) печатает беззнаковое число
  • .R ( n width – ) печатает знаковое число, дополненное пробелами до определенной ширины.

Например:

-123 6 .R

напечатет такие символы:

<space> <space> - 1 2 3

Другими словами, число дополняется до определенного количества символов.

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

Еще одна заминка в функции . и ее друзьях - это то, что они подчиняются текущей базе в переменной BASE. BASE может быть любым в диапазоне от 2 до 36.

Пока мы определяем . &c мы также можем определить .S которое является полезным инструментом отладки. Это слово печатает текущий стек (не разрушая его) сверху вниз.

Это основное рекурсивное определение U.:

: U. ( u -- )
    BASE @ U/MOD \ width rem quot
    ?DUP IF      \ if quotient <> 0 then
        RECURSE  \ print the quotient
    THEN

    \ печатаем остаток
    DUP 10 < IF
        '0'  \ десятичные цифры 0..9 )
    ELSE
        10 - \ шестнадцатиричные и другие цифры A..Z )
        'A'
    THEN
    +
    EMIT
;

Слово .S печатает содержимое стека. Оно не меняет стек. Очень полезно для отладки.

: .S ( -- )
    DSP@ \ взять текущий стековый указатель
    BEGIN
        DUP S0 @ <
    WHILE
            DUP @ U. \ напечатать элемент из стека
            SPACE
            4+       \ двигаться дальше
    REPEAT
    DROP \ сбросить указатель
;

Это слово возвращает ширину (в символах) числа без знака в текущей базе

: UWIDTH ( u -- width )
    BASE @ /        \ rem quot
    ?DUP IF         \ if quotient <> 0 then
        RECURSE 1+  \ return 1+recursive call
    ELSE
        1           \ return 1
    THEN
;

: U.R       ( u width -- )
    SWAP    ( width u )
    DUP     ( width u u )
    UWIDTH  ( width u uwidth )
    ROT     ( u uwidth width )
    SWAP -  ( u width-uwidth )
    ( В этот момент, если запрошенная ширина уже, у нас будет отрицательное число в стеке.
    В противном случае число в стеке - это количество пробелов для печати.
    Но SPACES не будет печатать отрицательное количество пробелов в любом случае,
    поэтому теперь можно безопасно вызвать SPACES ... )
    SPACES
    ( ... а затем вызвать базовую реализацию U. )
    U.
;

.R печатает беззнаковое число, дополненное определенной шириной. Мы не можем просто распечатать знак и вызвать U.R, потому что мы хотим, чтобы знак был рядом с номером ('-123' а не '- 123').

: .R  ( n width -- )
    SWAP        ( width n )
    DUP 0< IF
        NEGATE  ( width u )
        1       ( сохранить флаг, чтобы запомнить, что оно отрицательное | width n 1 )
        SWAP    ( width 1 u )
        ROT     ( 1 u width )
        1-      ( 1 u width-1 )
    ELSE
        0       ( width u 0 )
        SWAP    ( width 0 u )
        ROT     ( 0 u width )
    THEN
    SWAP        ( flag width u )
    DUP         ( flag width u u )
    UWIDTH      ( flag width u uwidth )
    ROT         ( flag u uwidth width )
    SWAP -      ( flag u width-uwidth )

    SPACES      ( flag u )
    SWAP        ( u flag )

    IF          ( число было отрицательным? печатаем минус )
        '-' EMIT
    THEN

    U.
;

Наконец, мы можем определить слово . через .R, с оконечными пробелами.

: . 0 .R SPACE ;

Реальный U., с оконечными пробелами.

: U. U. SPACE ;

Это слово выбирает целое число по адресу и печатает его.

: ? ( addr -- ) @ . ;

Еще полезные слова

c a b WITHIN возвращает true если a <= c and c < b

или можно определить его без IF : OVER - >R - R> U<

: WITHIN
    -ROT ( b c a )
    OVER ( b c a c )
    <= IF
        > IF ( b c -- )
            TRUE
        ELSE
            FALSE
        THEN
    ELSE
        2DROP ( b c -- )
        FALSE
    THEN
;

DEPTH возвращает глубину стека

: DEPTH        ( -- n )
    S0 @ DSP@ -
    4-         ( это нужно потому что Ы0 было на стеке, когда мы push-или DSP )
;

ALIGNED берет адрес и округляет его (выравнивает) к следующей границе 4 байта

: ALIGNED ( addr -- addr )
    3 + 3 INVERT AND \ (addr+3) & ~3
;

ALIGN выравнивает указатель HERE, поэтому следующее добавленное слово будет правильно выровнено.

: ALIGN HERE @ ALIGNED HERE ! ;

Строки

 S" string"  используется в Forth для определения строк. Это слово оставляет адрес строки и ее длину на вершине стека). Пробел, следующей за  S" , является нормальным пробелом между словами Forth и не является частью строки.

Это сложно определить, потому что он должен делать разные вещи в зависимости от того, компилируем мы или в находимся немедленном режиме. (Таким образом, слово помечено как IMMEDIATE, чтобы оно могло обнаружить это и делать разные вещи).

В режиме компиляции мы добавляем:

LITSTRING <string length> <string rounded up 4 bytes>

к текущему слову. Примитив LITSTRING делает все правильно, когда выполняется текущее слово.

В непосредственном режиме нет особого места для размещения строки, но в этом случае мы помещаем строку по адресу HERE (но мы не изменяем HERE). Это подразумевается как временное местоположение, которое вскоре будет перезаписано.

\ C, добавляет байт к текущему компилируемому слову
: C,
    HERE @ C! \ сохраняет символ в текущем компилируемом образе
    1 HERE +! \ увеличивает указатель HERE на 1 байт
;

: S" IMMEDIATE ( -- addr len )
    STATE @ IF           \ (компилируем)?
        ' LITSTRING ,    \ ?-Да: компилировать LITSTRING
        HERE @           \ сохранить адрес длины слова в стеке
        0 ,              \ фейковая длина - мы ее пока не знаем
        BEGIN
            KEY          \ взять следующий символ строки
            DUP '"' <>
        WHILE
                C,       \ копировать символ
        REPEAT
        DROP             \ сбросить символ двойной кавычки, которым заканчивалась строка
        DUP              \ получить сохраненный адрес длины слова
        HERE @ SWAP -    \ вычислить длину
        4-               \ вычесть 4 потому что мы измеряем от начала длины слова
        SWAP !           \ и заполнить длину )
        ALIGN            \ округить к следующему кратному 4 байту для оставшегося кода
    ELSE \ immediate mode
        HERE @           \ взять адрес начала временного пространства
        BEGIN
            KEY
            DUP '"' <>
        WHILE
                OVER C!  \ сохраниь следующий символ
                1+       \ увеличить адрес
        REPEAT
        DROP             \ сбросить символ двойной кавычки, которым заканчивалась строка
        HERE @ -         \ вычислить длину
        HERE @           \ push начальный адрес
        SWAP             ( addr len )
    THEN
;

 ."  является оператором печати строки в Forth. Пример:  ." Something to print"  Пробел после оператора - обычный пробел, требуемый между словами, и не является частью того, что напечатано.

В непосредственном режиме мы просто продолжаем читать символы и печатать их, пока не перейдем к следующей двойной кавычки.

В режиме компиляции мы используем  S"  для хранения строки, а затем добавляем TELL впоследствии:

LITSTRING <string length> <string rounded up to 4 bytes> TELL

Может быть интересно отметить использование [COMPILE], чтобы превратить вызов в непосредственное слово  S"  в компиляцию этого слова. Он компилирует его в определение  ." , а не в определение скомпилированного слова, когда оно выполняется

: ." IMMEDIATE ( -- )
    STATE @ IF       \ компиляция?
        [COMPILE] S" \ прочитать строку и скомпилировать LITSTRING, etc.
        ' TELL ,     \ скомпилировать окончательный TELL
    ELSE
        \ В немедленном режиме просто читаем символы и печаетем им пока не встретим кавычку
        BEGIN
            KEY
            DUP '"' = IF
                DROP \ сбросим со стека символ двойной кавычки
                EXIT \ возврат из функции
            THEN
            EMIT
        AGAIN
    THEN
;

Константы и переменные

В Forth глобальные константы и переменные определяются следующим образом:

10 CONSTANT TEN  # когда TEN выполняется, он оставляет целое число 10 в стеке
VARIABLE VAR     # когда VAR выполняется, он оставляет адрес VAR в стеке

Константы можно читать, но не писать, например:

TEN . CR # печатает 10

Вы можете прочитать переменную (в этом примере, называемую VAR), выполнив:

VAR @       # оставляет значение VAR в стеке
VAR @ . CR  # печатает значение VAR
VAR ? CR    # как и выше, поскольку ? такой же как @ .

и обновить переменную, выполнив:

20 VAR ! # записывает в VAR число 20

Обратите внимание, что переменные неинициализированы (но см. VALUE позже, в котором инициализированные переменные содержат несколько более простой синтаксис).

CONSTANT

Как мы можем определить слова CONSTANT и VARIABLE?

Трюк заключается в том, чтобы определить новое слово для самой переменной (например, если переменная называлась "VAR", тогда мы бы определили новое слово под названием VAR). Это легко сделать, потому что мы открыли создание словарных записей через слово CREATE (часть определения : выше). Вызов WORD [TEN] CREATE (где [TEN] означает, что "TEN" является следующим введенным словом) создает запись словаря:

forth-interpret-29.png

Для CONSTANT мы можем продолжить это, просто добавив DOCOL (как codeword), затем LIT, за которым следует сама константа, а затем EXIT, образуя небольшое определение слова, которое возвращает константу:

forth-interpret-30.png

Обратите внимание, что это определение слова точно такое же, как и у вас, если бы вы написали

: TEN 10 ;

Примечание для людей, читающих код ниже: DOCOL - это постоянное слово, которое мы определили в ассемблерной части.

: CONSTANT
    WORD     \ получить имя, которое следует за CONSTANT
    CREATE   \ создать заголовок элемента словаря
    DOCOL ,  \ добавить DOCOL как codeword поля слова
    ' LIT ,  \ добавить codeword LIT
    ,        \ добавить значение, которое лежит на вершине стека
    ' EXIT , \ добавить codeword EXIT
;

VARIABLE

VARIABLE немного сложнее, потому что нам нужно где-то вставить переменную. Нет ничего особенного в пользовательской памяти (область памяти, на которую указывает HERE, где мы ранее только хранили новые определения слов). Мы можем вырезать кусочки этой области памяти, чтобы сохранить что угодно, поэтому одно возможное определение VARIABLE может создать это:

forth-interpret-31.png

Чтобы сделать это более общим, давайте определим пару слов, которые мы можем использовать для выделения произвольной памяти из пользовательской памяти.

Первое из них - ALLOT, где n ALLOT выделяет n байтов памяти. (Обратите внимание, что при вызове ALLOT стоит, убедиться, что n кратно 4, или, по крайней мере, в следующий раз, когда слово скомпилировано, что HERE осталось кратным 4).

: ALLOT ( n -- addr )
    HERE @ SWAP ( here n )
    HERE +!     \ добавляем n к HERE, после этого старое значение остается на стеке
;

Второе важное слово - CELLS. В Forth выражение n CELLS ALLOT означает выделение n integer-ов любого размера - это натуральный размер для integer в этой машинной архитектуре. На этой 32-битной машине CELLS просто увеличивает вершину стека на 4.

: CELLS ( n -- n ) 4 * ;

Итак, теперь мы можем легко определить переменную во многом так же, как и CONSTANT выше. См. схему выше, чтобы увидеть, как будет выглядеть слово, которое создает VARIABLE.

: VARIABLE
    1 CELLS ALLOT \ выделить 4 байтовую ячейку для integer в памяти, push указатель на нее
    WORD CREATE   \ создать элемент словаря, имя которого следует за VARIABLE
    DOCOL ,       \ добавить DOCOL  как поле codeword этого слова
    ' LIT ,       \ добавить codeword LIT
    ,             \ добавить указатель на выделенную память
    ' EXIT ,      \ добавить codeword EXIT
;

VALUE

VALUE похожи на VARIABLE, но с более простым синтаксисом. Вы обычно используете их, когда вам нужна переменная, которая часто читается, а записывается нечасто.

20 VALUE VAL \ создаем VAL и инициализируем ее значением 20
VAL          \ push-им значение переменной VAL (20) в стек
30 TO VAL    \ изменяем VAL, устанавливае ее в 30
VAL          \ push-им новое значение переменной VAL (30) в стек

Обратите внимание, что «VAL» сам по себе не возвращает адрес значения, а само значение, делая значения более понятными и приятными для использования, чем переменные (без косвенности через «@»). Цена представляет собой более сложную реализацию, хотя, несмотря на сложность, во время исполнения нет штрафа за производительность.

Наивная реализация "TO" была бы довольно медленной, каждый раз ей приходилось бы искать в словаре. Но поскольку это Forth, мы имеем полный контроль над компилятором, чтобы мы могли бы более эффективно компилировать TO, превращая:

TO VAL

в

LIT <addr> !

и вычислить <addr> (адрес значения) во время компиляциии

Теперь это довольно умно. Мы скомпилируем наше значение следующим образом:

forth-interpret-32.png

где <value> - это фактическое значение. Обратите внимание, что когда VAL выполняется, он будет выталкивать значение в стек, чего мы и хотим.

Но что будет использовать для адреса <addr>? Разумеется, указатель на этот <value>:

forth-interpret-33.png

Другими словами, это своего рода самомодифицирующийся код.

(Замечение для людей, которые хотят изменить этот Forth, чтобы добавить инлайнинг: значения, определенные таким образом, не могут быть заинлайнены).

: VALUE ( n -- )
    WORD CREATE \ создаем заголовок элемента словаря - имя следует за VALUE
    DOCOL ,     \ добавляем DOCOL
    ' LIT ,     \ добавляем codeword LIT
    ,           \ добавляем начальное значение
    ' EXIT ,    \ добавляем codeword EXIT
;

: TO IMMEDIATE ( n -- )
    WORD        \ получаем имя VALUE
    FIND        \ ищем его в словаре
    >DFA        \ получаем указатель на первое поле данных -'LIT'
    4+          \ увеличиваем его значение на размер данных
    STATE @ IF \ компиляция?
        ' LIT , \ да, компилировать LIT
        ,       \ компилировать адрес значения
        ' ! ,   \ компилировать !
    ELSE       \ нет, immediate mode
        !       \ обновить сразу
    THEN
;

x +TO VAL добавляет x к VAL

: +TO IMMEDIATE
    WORD \ получаем имя значения
    FIND \ ищем в словаре
    >DFA \ получаем указатель на первое поле данных -'LIT'
    4+   \ увеличиваем его значение на размер данных
    STATE @ IF \ компиляция?
        ' LIT , \ да, компилировать LIT
        ,       \ компилировать адрес значения
        ' +! ,  \ компилировать +!
    ELSE \ нет, immediate mode
        +! \ обновить сразу
    THEN
;

Печать словаря

ID. берет адрес словаря и печатает имя слова.

Например: LATEST @ ID. распечатает имя последнего определенного слова

: ID.
    4+            ( перепрыгиваем через указатель link )
    DUP C@        ( получаем байт flags/length )
    F_LENMASK AND ( маскируем flags - мы хотим просто получить длину )

    BEGIN
        DUP 0>    ( длина > 0? )
    WHILE
            SWAP 1+ ( addr len -- len addr+1 )
            DUP C@  ( len addr -- len addr char | получаем следующий символ )
            EMIT    ( len addr char -- len addr | и печатаем его )
            SWAP 1- ( len addr -- addr len-1    | вычитаем единицу из длины )
    REPEAT
    2DROP         ( len addr -- )
;

WORD word FIND ?HIDDEN возвращает true, если слово word помечено как скрытое. WORD word FIND ?IMMEDIATE возвращает true, если слово word помечен как "немедленное".

: ?HIDDEN
    4+ ( перепрыгиваем через указатель link )
    C@ ( получаем байт flags/length )
    F_HIDDEN AND ( маскируем F_HIDDEN флаг и возвращаем его )
;

: ?IMMEDIATE
    4+ ( перепрыгиваем через указатель link )
    C@ ( получаем байт flags/length )
    F_IMMED AND ( маскируем  F_IMMED флаг и возвращаем его )
;

WORDS печатает все слова, определенные в словаре, начиная с самого последнего слова. Однако оно не печатает скрытые слова. Реализация просто двигается назад от LATEST с помощью ссылок-указателей.

: WORDS
    LATEST @ ( начинаем с LATEST указателя )
    BEGIN
        ?DUP ( полка указатель не null )
    WHILE
            DUP ?HIDDEN NOT IF ( игнорируем скрытые слова )
                DUP ID.        ( если не скрытое, то печатаем слово )
                SPACE
            THEN
            @ ( dereference link - идем к следующему слову )
    REPEAT
    CR
;

Забывание

До сих пор мы только выделяли память для слов. Forth обеспечивает довольно примитивный метод освобождения.

FORGET word удаляет определение «слова» из словаря и всего, что определено после него, включая любые переменные и другую память, выделенную после.

Реализация очень проста - мы просматриваем слово (которое возвращает адрес записи словаря). Затем мы устанавливаем HERE, чтобы указывать на этот адрес, так что все будущие распределения и определения будут перезаписывать память, начиная с него. Нам также необходимо установить LATEST, чтобы указать на предыдущее слово.

Обратите внимание: вы не можете FORGET встроенные слова (ну, вы можете попробовать, но это, вероятно, вызовет segfault).

XXX: Поскольку мы написали VARIABLE, чтобы сохранить переменную в памяти, выделенную до слова, в текущей реализации VARIABLE FOO FORGET FOO приведет к утечке одной ячейки памяти.

: FORGET
    WORD FIND      ( найти слов и получить его dictionary entry address )
    DUP @ LATEST ! ( установить LATEST на указатель предыдущего слова )
    HERE !         ( и сохранить HERE как dictionary address )
;

Дамп

DUMP используется для выгрузки содержимого памяти в "традиционном" формате hexdump.

Обратите внимание, что параметры DUMP (адрес, длина) совместимы со строковыми словами, такими как WORD и S".

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

LATEST @ 128 DUMP

Вот реализация:

: DUMP ( addr len -- )
    BASE @ -ROT ( save the current BASE at the bottom of the stack )
    HEX ( and switch to hexadecimal mode )

    BEGIN
        ?DUP ( while len > 0 )
    WHILE
            OVER 8 U.R ( print the address )
            SPACE

            ( print up to 16 words on this line )
            2DUP ( addr len addr len )
            1- 15 AND 1+ ( addr len addr linelen )
            BEGIN
                ?DUP ( while linelen > 0 )
            WHILE
                    SWAP ( addr len linelen addr )
                    DUP C@ ( addr len linelen addr byte )
                    2 .R SPACE ( print the byte )
                    1+ SWAP 1- ( addr len linelen addr -- addr len addr+1 linelen-1 )
            REPEAT
            DROP ( addr len )

            ( print the ASCII equivalents )
            2DUP 1- 15 AND 1+  ( addr len addr linelen )
            BEGIN
                ?DUP ( while linelen > 0)
            WHILE
                    SWAP ( addr len linelen addr )
                    DUP C@ ( addr len linelen addr byte )
                    DUP 32 128 WITHIN IF ( 32 <= c < 128? )
                        EMIT
                    ELSE
                        DROP '.' EMIT
                    THEN
                    1+ SWAP 1- ( addr len linelen addr -- addr len addr+1 linelen-1 )
            REPEAT
            DROP ( addr len )
            CR

            DUP 1- 15 AND 1+  ( addr len linelen )
            TUCK ( addr linelen len linelen )
            - ( addr linelen len-linelen )
            >R + R> ( addr+linelen len-linelen )
    REPEAT

    DROP ( restore stack )
    BASE ! ( restore saved BASE )
;

Case

CASE ... ENDCASE - это то, как мы делаем switch в Forth. Для этого нет общего согласованного синтаксиса, поэтому я реализовал синтаксис, предусмотренный стандартом ISO Forth (ANS-Forth).

( some value on the stack )
CASE
    test1 OF ... ENDOF
    test2 OF ... ENDOF
    testn OF ... ENDOF
    ... ( default case )
ENDCASE

Оператор CASE проверяет значение в стеке, проверяя его на равенство с test1, test2, …, testn и выполняет соответствующий фрагмент кода внутри OF … ENDOF. Если ни одно из тестовых значений не совпадает, выполняется случай по умолчанию. Внутри … случая по умолчанию значение все еще находится в верхней части стека (оно неявно DROP-нется с помощью ENDCASE). Когда ENDOF выполняется, он перескакивает после ENDCASE (т. e. Отсутствует 2провал" и нет необходимости в операторе break, как в C).

default case может быть опущен. Фактически tests также могут быть опущены, так что у вас будет только default case, хотя это, вероятно, не очень полезно.

Пример (предполагая, что «q» и т. Д. - это слова, которые push-ат значение ASCII-кода буквы в стек):

0 VALUE QUIT
0 VALUE SLEEP
KEY CASE
    'q' OF 1 TO QUIT ENDOF
    's' OF 1 TO SLEEP ENDOF
    ( default case: )
    ." Sorry, I didn't understand key <" DUP EMIT ." >, try again." CR
ENDCASE

В некоторых версиях Forth поддерживаются более продвинутые tests, такие как диапазоны и.т.д. В других версиях Forth вам нужно написать OTHERWISE, чтобы указать default case. Как я сказал выше, этот Forth пытается следовать стандарту ANS Forth.

Реализация CASE … ENDCASE несколько нетривиальна. Я следовал этой реализации: http://www.uni-giessen.de/faq/archiv/forthfaq.case_endcase/msg00000.html (в данный момент недоступна)

Общий план состоит в том, чтобы скомпилировать код как ряд операторов IF:

CASE                          \ (push 0 on the immediate-mode parameter stack)
    test1 OF ... ENDOF        \ test1 OVER = IF DROP ... ELSE
    test2 OF ... ENDOF        \ test2 OVER = IF DROP ... ELSE
    testn OF ... ENDOF        \ testn OVER = IF DROP ... ELSE
    ...  ( default case )...
ENDCASE                       \ DROP THEN [THEN [THEN ...]]

Оператор CASE push-ит 0 на стек параметров в "немедленном" режиме, и это число используется для подсчета количества инструкций THEN, которые нам нужны, когда мы получаем ENDCASE, чтобы каждый IF имел соответствующий THEN. Подсчет делается неявно. Если вы помните из реализации выше IF, каждый IF push-ит адрес кода в стеке в немедленном режиме, и эти адреса не равны нулю, поэтому к тому времени, когда мы дойдем до ENDCASE, стек содержит некоторое количество ненулевых элементов, а затем нуль. Число ненулевых чисел - это сколько раз IF был вызван, поэтому сколько же раз мы должны сделать соответствующий THEN.

Этот код использует [COMPILE], чтобы мы скомпилировали вызовы IF, ELSE, THEN, а не вызывали их во время компиляции слов ниже.

Как и во всех наших структурах управления, они работают только в определениях слов, а не в непосредственном режиме.

: CASE IMMEDIATE
    0 ( push 0 to mark the bottom of the stack )
;

: OF IMMEDIATE
    ' OVER , ( compile OVER )
    ' = , ( compile = )
    [COMPILE] IF ( compile IF )
    ' DROP ,   ( compile DROP )
;

: ENDOF IMMEDIATE
    [COMPILE] ELSE ( ENDOF is the same as ELSE )
;

: ENDCASE IMMEDIATE
    ' DROP , ( compile DROP )

    ( keep compiling THEN until we get to our zero marker )
    BEGIN
        ?DUP
    WHILE
            [COMPILE] THEN
    REPEAT
;

Декомпилятор

CFA> является противоположностью >CFA. Он принимает codeword и пытается найти подходящее определение словаря. (По правде говоря, он работает с любым указателем на слово, а не только c указателем на codeword, и это необходимо для выполнения трассировки стека).

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

Это слово возвращает 0, если ничего не находит

: CFA>
    LATEST @ ( start at LATEST dictionary entry )
    BEGIN
        ?DUP ( while link pointer is not null )
    WHILE
            2DUP SWAP ( cfa curr curr cfa )
            < IF ( current dictionary entry < cfa? )
                NIP ( leave curr dictionary entry on the stack )
                EXIT
            THEN
            @ ( follow link pointer back )
    REPEAT
    DROP ( restore stack )
    0 ( sorry, nothing found )
;

SEE декомпилирует слово Forth.

Мы ищем dictionary entry слова, затем снова ищем опять для следующего слова (фактически, конец скомпилированного слова). Это приводит к двум указателям:

forth-interpret-34.png

С этой информацией мы можем декомпилировать слово. Нам нужно узнавать "мета-слова", такие как LIT, LITSTRING, BRANCH и.т.д. И обрабатывать их особенным образом.

: SEE
    WORD FIND ( find the dictionary entry to decompile )

    ( Now we search again, looking for the next word in the dictionary.  This gives us
    the length of the word that we will be decompiling.   (Well, mostly it does). )
    HERE @ ( address of the end of the last compiled word )
    LATEST @ ( word last curr )
    BEGIN
        2 PICK ( word last curr word )
        OVER ( word last curr word curr )
        <> ( word last curr word<>curr? )
    WHILE ( word last curr )
            NIP ( word curr )
            DUP @ ( word curr prev  (which becomes: word last curr) )
    REPEAT

    DROP ( at this point, the stack is: start-of-word end-of-word )
    SWAP ( end-of-word start-of-word )

    ( begin the definition with : NAME [IMMEDIATE] )
    ':' EMIT SPACE DUP ID. SPACE
    DUP ?IMMEDIATE IF ." IMMEDIATE " THEN

    >DFA ( get the data address, ie. points after DOCOL | end-of-word start-of-data )

    ( now we start decompiling until we hit the end of the word )
    BEGIN ( end start )
        2DUP >
    WHILE
            DUP @ ( end start codeword )

            CASE
                ' LIT OF ( is it LIT ? )
                    4 + DUP @ ( get next word which is the integer constant )
                    . ( and print it )
                ENDOF
                ' LITSTRING OF ( is it LITSTRING ? )
                    [ CHAR S ] LITERAL EMIT '"' EMIT SPACE  ( print S"<space> )
                    4 + DUP @ ( get the length word )
                    SWAP 4 + SWAP ( end start+4 length )
                    2DUP TELL ( print the string )
                    '"' EMIT SPACE ( finish the string with a final quote )
                    + ALIGNED ( end start+4+len, aligned )
                    4 - ( because we're about to add 4 below )
                ENDOF
                ' 0BRANCH OF ( is it 0BRANCH ? )
                    ." 0BRANCH  ( "
                    4 + DUP @ ( print the offset )
                    .
                    ." ) "
                ENDOF
                ' BRANCH OF ( is it BRANCH ? )
                    ." BRANCH  ( "
                    4 + DUP @ ( print the offset )
                    .
                    ." ) "
                ENDOF
                ' ' OF ( is it '  (TICK) ? )
                    [ CHAR ' ] LITERAL EMIT SPACE
                    4 + DUP @ ( get the next codeword )
                    CFA> ( and force it to be printed as a dictionary entry )
                    ID. SPACE
                ENDOF
                ' EXIT OF ( is it EXIT? )
                    ( We expect the last word to be EXIT, and if it is then we don't print it
                    because EXIT is normally implied by ;.  EXIT can also appear in the middle
                    of words, and then it needs to be printed. )
                    2DUP ( end start end start )
                    4 + ( end start end start+4 )
                    <> IF ( end start | we're not at the end )
                        ." EXIT "
                    THEN
                ENDOF
                ( default case: )
                DUP ( in the default case we always need to DUP before using )
                CFA> ( look up the codeword to get the dictionary entry )
                ID. SPACE ( and print it )
            ENDCASE

            4 + ( end start+4 )
    REPEAT

    ';' EMIT CR

    2DROP ( restore stack )
;

Токены выполнения

Стандарт Forth определяет концепцию, называемую "токеном выполнения" (или "xt"), которая очень похожа на указатель функции в Си. Мы сопоставляем токен выполнения с адресом кодового слова.

forth-interpret-35.png

Существует один ассемблерный примитив для выполнения токенов, EXECUTE (xt -), который их запускает.

Вы можете сделать токен выполнения для существующего слова длинным путем, используя >CFA, то есть: WORD [foo] FIND >CFA будет push-ить xt для foo в стек, где foo - следующее введенное слово. Таким образом, очень медленный способ запуска DOUBLE может быть:

: DOUBLE DUP + ;
: SLOW WORD FIND >CFA EXECUTE ;

5 SLOW DOUBLE . CR \ prints 10

Мы также предлагаем более простой и быстрый способ получить токен выполнения любого слова FOO:

['] FOO

Домашнее задание:

  • (1) Какая разница между ['] FOO и ~' FOO~?
  • (2) Как соотносятся ~'~, ['] и LIT?

Более полезным является определение анонимных слов и/или присваивание переменным токенов выполнения (xt).

Чтобы определить анонимное слово (и запушить его xt в стеке), используйте: NONAME ...; как в этом примере:

:NONAME ." anon word was called" CR ; \ push-ит xt в стек

DUP EXECUTE EXECUTE  \ выполянет анонимное слово дважды

Параметры в стеке тоже работают должным образом:

:NONAME ." called with parameter " . CR ;
DUP
10 SWAP EXECUTE \ напечатает 'called with parameter 10'
20 SWAP EXECUTE \ напечатает 'called with parameter 20'

Обратите внимание, что вышеупомянутый код создает утечку памяти: анонимное слово все еще компилируется в сегмент данных, поэтому, даже если вы потеряете отслеживание xt, слово продолжает занимать память. Хороший способ отслеживания xt и, таким образом, избежать утечки памяти - назначить его CONSTANT, VARIABLE или VALUE:

0 VALUE ANON
:NONAME ." anon word was called" CR ; TO ANON
ANON EXECUTE
ANON EXECUTE

Еще одно использование :NONAME - создание массива функций, которые можно быстро вызвать (подумайте о быстром switch например). Этот пример адаптирован из стандарта ANS Forth:

10 CELLS ALLOT CONSTANT CMD-TABLE
: SET-CMD CELLS CMD-TABLE + ! ;
: CALL-CMD CELLS CMD-TABLE + @ EXECUTE ;

:NONAME ." alternate 0 was called" CR ; 0 SET-CMD
:NONAME ." alternate 1 was called" CR ; 1 SET-CMD
\ etc...
:NONAME ." alternate 9 was called" CR ; 9 SET-CMD

0 CALL-CMD
1 CALL-CMD

Итак, реализуем :NONAME и [']:

: :NONAME
    0 0 CREATE ( create a word with no name - we need a dictionary header because ; expects it )
    HERE @     ( current HERE value is the address of the codeword, ie. the xt )
    DOCOL ,    ( compile DOCOL  (the codeword) )
    ]          ( go into compile mode )
;

: ['] IMMEDIATE
    ' LIT ,    ( compile LIT )
;

Исключения

(об истории появления исключений и и причинах принятых решений можно прочитать тут: CATCH и THROW)

Удивительно, но исключения могут быть реализованы непосредственно в Forth, на самом деле довольно легко.

Общее использование:

: FOO ( n -- ) THROW ;

: TEST-EXCEPTIONS
    25 ['] FOO CATCH \ execute 25 FOO, catching any exception
    ?DUP IF
        ." called FOO and it threw exception number: "
        . CR
        DROP \ we have to drop the argument of FOO (25)
    THEN
;
\ prints: called FOO and it threw exception number: 25

CATCH запускает токен выполнения и определяет, выбрасывает ли оно какое-либо исключение или нет. Стековая сигнатура CATCH довольно сложна:

( a_n-1 ... a_1 a_0 xt -- r_m-1 ... r_1 r_0 0 ) \ если xt не выбрасывает exception
( a_n-1 ... a_1 a_0 xt -- ?_n-1 ... ?_1 ?_0 e ) \ если xt выбрасывает exception 'e'

где ai и ri - это (произвольное число) аргументов и содержимое стека возврата до и после того, как xt выполнен с помощью EXECUTE. Обратите внимание, в частности, на такой случай: когда генерируется исключение, указатель стека восстанавливается так, что в стеке есть n из something в позициях, где раньше были аргументы a_i. Мы действительно не гарантируем, что находится в стеке - возможно, исходные аргументы а, возможно, какая-то другая ерунда - это во многом зависит от реализации слова, которое выполнялось.

THROW, ABORT и еще несколько других исключений.

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

0 THROW ничего не делает. Вот стековая сигнатура THROW:

( 0 -- )
( * e -- ?_n-1 ... ?_1 ?_0 e ) \ the stack is restored to the state
                               \ from the corresponding CATCH

Реализация зависит от определений CATCH и THROW и состояния, разделяемого между ними.

До этого момента стек возврата состоял только из списка адресов возврата, причем вершина возвращаемого стека была обратным адресом, где мы возобновляем выполнение, когда текущее слово делает EXIT. Однако CATCH будет push-ить более сложный фрейм стека исключений в стек возврата. Фрейм стека исключений записывает некоторые вещи о состоянии выполнения в момент вызова CATCH.

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

Это то, как выглядит фрейм стека исключений. (Как обычно, стек возвратов показан вниз, от более младших до более старших адресов памяти, а растет он вверх).

forth-interpret-36.png

EXCEPTION-MARKER отмечает эту запись как фрейм стека исключений, а не обычный обратный адрес, и именно это THROW "замечает", поскольку оно разматывает стек. (Если вы хотите внедрить более сложные исключения, такие как TRY … WITH, тогда вам нужно будет использовать другое значение маркера, если вы хотите, чтобы старые маркеры фреймов стека и новые исключения сосуществовали).

Что произойдет, если исполняемое слово не выбрасывает исключение? Он, в конце концов, вернется и вызовет EXCEPTION-MARKER, поэтому EXCEPTION-MARKER лучше сделать что-то разумное без необходимости изменения EXIT. Это красиво дает нам подходящее определение EXCEPTION-MARKER, а именно функцию, которая просто отбрасывает кадр стека и сама возвращается (таким образом, "возвращается" из исходного CATCH).

Из этого следует, что исключения - относительно легкий механизм в Forth.

: EXCEPTION-MARKER
    RDROP ( drop the original parameter stack pointer )
    0 ( there was no exception, this is the normal return path )
;

: CATCH ( xt -- exn? )
    DSP@ 4+ >R ( save parameter stack pointer  (+4 because of xt) on the return stack )
    ' EXCEPTION-MARKER 4+ ( push the address of the RDROP inside EXCEPTION-MARKER ... )
    >R ( ... on to the return stack so it acts like a return address )
    EXECUTE ( execute the nested function )
;

: THROW ( n -- )
    ?DUP IF ( only act if the exception code <> 0 )
        RSP@  ( get return stack pointer )
        BEGIN
            DUP R0 4- < ( RSP < R0 )
        WHILE
                DUP @ ( get the return stack entry )
                ' EXCEPTION-MARKER 4+ = IF ( found the EXCEPTION-MARKER on the return stack )
                    4+ ( skip the EXCEPTION-MARKER on the return stack )
                    RSP! ( restore the return stack pointer )

                    ( Restore the parameter stack. )
                    DUP DUP DUP ( reserve some working space so the stack for this word
                    doesn't coincide with the part of the stack being restored )
                    R> ( get the saved parameter stack pointer | n dsp )
                    4- ( reserve space on the stack to store n )
                    SWAP OVER ( dsp n dsp )
                    ! ( write n on the stack )
                    DSP! EXIT ( restore the parameter stack pointer, immediately exit )
                THEN
                4+
        REPEAT

        ( No matching catch - print a message and restart the INTERPRETer. )
        DROP

        CASE
            0 1- OF ( ABORT )
                ." ABORTED" CR
            ENDOF
            ( default case )
            ." UNCAUGHT THROW "
            DUP . CR
        ENDCASE
        QUIT
    THEN
;

: ABORT ( -- )
    0 1- THROW
;


( Print a stack trace by walking up the return stack. )
: PRINT-STACK-TRACE
    RSP@ ( start at caller of this function )
    BEGIN
        DUP R0 4- < ( RSP < R0 )
    WHILE
            DUP @ ( get the return stack entry )
            CASE
                ' EXCEPTION-MARKER 4+ OF ( is it the exception stack frame? )
                    ." CATCH  ( DSP="
                    4+ DUP @ U. ( print saved stack pointer )
                    ." ) "
                ENDOF
                ( default case )
                DUP
                CFA> ( look up the codeword to get the dictionary entry )
                ?DUP IF ( and print it )
                    2DUP ( dea addr dea )
                    ID. ( print word from dictionary entry )
                    [ CHAR + ] LITERAL EMIT
                    SWAP >DFA 4+ - . ( print offset )
                THEN
            ENDCASE
            4+ ( move up the stack )
    REPEAT
    DROP
    CR
;

Строки языка Си

Строки Forth представлены начальным адресом и длиной, хранящейся в стеке или в памяти.

Большинство Forth-ов не обрабатывают строки Си, но мы нуждаемся в них, чтобы получить доступ к аргументам процесса и окружающей среде, оставленным в стеке ядром Linux, и сделать некоторые системные вызовы.

Операция Input Output Forth word Notes
Создание Forth-строк addr len S" …"    
Создание C-строк c-addr Z" …"    
C -> Forth c-addr addr len DUP STRLEN  
Forth -> C addr len c-addr CSTRING Аллоцируются во
        временном буфере
        и должны быть
        использованы или
        скопированы сразу.
        И не должны
        содержать NULs

Например, DUP STRLEN TELL печатает строку C.

Z" …" очень похожа на S" …" за исключением того, что строка заканчивается символом ASCII NUL.

Чтобы сделать его более похожим на строку C, во время выполнения Z" просто оставляет адрес строки в стеке (а не адрес и длину, как ~S"~) Чтобы реализовать это, нам нужно добавить дополнительный NUL в строку, а затем инструкцию DROP. Кроме этого, эта реализация является лишь модифицированной S".

: Z" IMMEDIATE
    STATE @ IF ( compiling? )
        ' LITSTRING , ( compile LITSTRING )
        HERE @ ( save the address of the length word on the stack )
        0 , ( dummy length - we don't know what it is yet )
        BEGIN
            KEY  ( get next character of the string )
            DUP '"' <>
        WHILE
                HERE @ C! ( store the character in the compiled image )
                1 HERE +! ( increment HERE pointer by 1 byte )
        REPEAT
        0 HERE @ C! ( add the ASCII NUL byte )
        1 HERE +!
        DROP ( drop the double quote character at the end )
        DUP ( get the saved address of the length word )
        HERE @ SWAP - ( calculate the length )
        4- ( subtract 4  (because we measured from the start of the length word) )
        SWAP ! ( and back-fill the length location )
        ALIGN ( round up to next multiple of 4 bytes for the remaining code )
        ' DROP , ( compile DROP  (to drop the length) )
    ELSE ( immediate mode )
        HERE @ ( get the start address of the temporary space )
        BEGIN
            KEY
            DUP '"' <>
        WHILE
                OVER C! ( save next character )
                1+ ( increment address )
        REPEAT
        DROP ( drop the final " character )
        0 SWAP C! ( store final ASCII NUL )
        HERE @ ( push the start address )
    THEN
;

: STRLEN  ( str -- len )
    DUP ( save start address )
    BEGIN
        DUP C@ 0<> ( zero byte found? )
    WHILE
            1+
    REPEAT

    SWAP - ( calculate the length )
;

: CSTRING ( addr len -- c-addr )
    SWAP OVER ( len saddr len )
    HERE @ SWAP ( len saddr daddr len )
    CMOVE ( len )

    HERE @ + ( daddr+len )
    0 SWAP C! ( store terminating NUL char )

    HERE @  ( push start address )
;

Окружение

Linux делает аргументы процесса и переменные окружения доступными нам в стеке.

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

Начав с S0, сам S0 указывает на argc (количество аргументов командной строки).

S0+4 указывает на argv[ 0 ], S0+8 указывает на argv[ 1 ] etc до argv[ argc-1 ].

argv[ argc ] это NULL указатель

После этого стек содержит переменные окружения - набор указателей на строки формы NAME=VALUE до тех пор, пока мы не перейдем к другому указателю NULL.

Первое слово, которое мы определяем, ARGC, push-ит количество аргументов командной строки (обратите внимание, что как и в случае с Сишным argc, это включает в себя имя программы).

: ARGC
    S0 @ @
;

n ARGV получаетет "энный" аргумент командной строки

Например, чтобы напечатать имя программы, вы сделали бы:

0 ARGV TELL CR

Вот реализация

: ARGV  ( n -- str u )
    1+ CELLS S0 @ + ( get the address of argv[n] entry )
    @ ( get the address of the string )
    DUP STRLEN ( and get its length / turn it into a Forth string )
;

ENVIRON возвращает адрес первой строки переменных окружения. Список строк заканчивается указателем NULL.

Например, чтобы напечатать первую строку переменных окружения, вы могли бы сделать:

ENVIRON @ DUP STRLEN TELL

Реализация:

: ENVIRON   ( -- addr )
    ARGC    ( number of command line parameters on the stack to skip )
    2 +     ( skip command line count and NULL pointer after the command line args )
    CELLS   ( convert to an offset )
    S0 @ +  ( add to base stack address )
;

Системные вызовы и файлы

Различные слова, связанные с системными вызовами, и стандартный доступ к файлам.

BYE вызывается, вызывая системный вызов выхода Linux (2).

: BYE ( -- )
    0 ( return code  (0) )
    SYS_EXIT ( system call number )
    SYSCALL1
;

UNUSED возвращает количество ячеек, оставшихся в пользовательской памяти (в сегменте данных).

Для нашей реализации мы будем использовать системный вызов Linux brk (2), чтобы узнать конец сегмента данных и вычесть HERE из него.

(
: GET-BRK ( -- brkpoint )
    0 SYS_BRK SYSCALL1 ( call brk (0) )
;

: UNUSED ( -- n )
    GET-BRK ( get end of data segment according to the kernel )
    HERE @ ( get current position in data segment )
    -
    4 / ( returns number of cells )
;
)

MORECORE увеличивает сегмент данных на указанное количество (4-х байтовых) ячеек.

NB. Количество запрошенных ячеек должно быть, как правило, кратным 1024. Причина в том, что Linux не может расширить сегмент данных менее чем на одну страницу (4096 байт или 1024 ячейки).

Этот Forth автоматически не увеличивает размер сегмента данных "по запросу" (т.е. Когда используются (COMMA), ALLOT, CREATE и.т.д.). Вместо этого программист должен знать, сколько места займет большое выделение, провеить UNUSED и вызвать MORECORE, если это необходимо. Простым упражнением для читаетеля является изменение реализации сегмента данных, так что MORECORE вызывается автоматически, если программе требуется больше памяти.

(
: BRK( brkpoint -- )
    SYS_BRK SYSCALL1
;

: MORECORE( cells -- )
    CELLS GET-BRK + BRK
;
)

Стандарт Forth предоставляет некоторые простые примитивы доступа к файлам, которые мы моделируем поверх системных вызовов Linux.

Главным осложнением является преобразование строк Forth (адрес и длина) в строки Си для ядра Linux.

Обратите внимание, что в этой реализации нет буферизации.

: R/O  ( -- fam ) O_RDONLY ;
: R/W  ( -- fam ) O_RDWR ;

: OPEN-FILE ( addr u fam -- fd 0  (if successful) | c-addr u fam -- fd errno  (if there was an error) )
    -ROT ( fam addr u )
    CSTRING ( fam cstring )
    SYS_OPEN SYSCALL2  ( open  (filename, flags) )
    DUP ( fd fd )
    DUP 0< IF ( errno? )
        NEGATE ( fd errno )
    ELSE
        DROP 0 ( fd 0 )
    THEN
;

: CREATE-FILE ( addr u fam -- fd 0  (if successful) | c-addr u fam -- fd errno  (if there was an error) )
    O_CREAT OR
    O_TRUNC OR
    -ROT ( fam addr u )
    CSTRING ( fam cstring )
    420 -ROT ( 0644 fam cstring )
    SYS_OPEN SYSCALL3  ( open  (filename, flags|O_TRUNC|O_CREAT, 0644) )
    DUP ( fd fd )
    DUP 0< IF ( errno? )
        NEGATE ( fd errno )
    ELSE
        DROP 0 ( fd 0 )
    THEN
;

: CLOSE-FILE ( fd -- 0  (if successful) | fd -- errno  (if there was an error) )
    SYS_CLOSE SYSCALL1
    NEGATE
;

: READ-FILE ( addr u fd -- u2 0  (if successful) | addr u fd -- 0 0  (if EOF) | addr u fd -- u2 errno  (if error) )
    >R SWAP R> ( u addr fd )
    SYS_READ SYSCALL3

    DUP ( u2 u2 )
    DUP 0< IF ( errno? )
        NEGATE ( u2 errno )
    ELSE
        DROP 0 ( u2 0 )
    THEN
;

\ PERROR prints a message for an errno, similar to C's perror (3) but we don't have the extensive
\ list of strerror strings available, so all we can do is print the errno.
: PERROR ( errno addr u -- )
    TELL
    ':' EMIT SPACE
    ." ERRNO="
    . CR
;

Это всего лишь схема простого ассемблера, позволяющая писать примитивы Forth на языке ассемблера прямо изнутри Fort-а

Ассемблерные примитивы начинаются с : NAME обычным способом, но заканчиваются :CODE.

;CODE обновляет заголовок так, что кодовое слово не является DOCOL, а указывает вместо этого на собранный код (в части DFA слова).

Мы предоставляем удобный макрос NEXT (вы догадались, что он делает). Однако вам не нужно использовать его, потому что CODE поместит NEXT в конец вашего слова.

Остальное состоит из некоторых непосредственных слов, которые расширяются в машинный код, прилагаемый к определению слова. Только крошечная часть сборки сборки i386 покрыта, достаточно, чтобы написать несколько ассемблерных примитивов ниже.

Для того чтобы иметь возможность скомпилировать макрос NEXT из Forth-кода мы просто создадим словоб которое побайтов вкомпилит LODSL | JMP *(%EAX) в создаваемое слово. LODSL ассемблируется как байт AD, а JMP *(%EAX) как байты FF 20 - это можно увидеть в отладчике или дизассемблере, после компиляции нашего ассемблерного файла.

HEX

: NEXT IMMEDIATE AD C, FF C, 20 C, ; \ NEXT эквивалент

: ;CODE IMMEDIATE
    [COMPILE] NEXT        \ заканчиваем слово с помощью NEXT
    ALIGN                 \ машинный код собирается побайтово, поэтому м.б. не выровнен
    LATEST @ DUP          \ получить значение LATEST и сделать еще одну его копию в стеке
    HIDDEN                \ unhide - забирает одно значение LATEST из стека
    DUP >DFA SWAP >CFA !  \ изменяем codeword чтобы он указывал на param-field
    [COMPILE] [           \ вкомпилить [ чтобы вернуться immediate mode
;

\ Регистры
: EAX IMMEDIATE 0 ;
: ECX IMMEDIATE 1 ;
: EDX IMMEDIATE 2 ;
: EBX IMMEDIATE 3 ;
: ESP IMMEDIATE 4 ;
: EBP IMMEDIATE 5 ;
: ESI IMMEDIATE 6 ;
: EDI IMMEDIATE 7 ;

\ Стековые инструкции
: PUSH IMMEDIATE 50 + C, ;
: POP IMMEDIATE 58 + C, ;

\ RDTSC
: RDTSC IMMEDIATE 0F C, 31 C, ;

DECIMAL

\ RDTSC является ассемблерным примитивом, который считывает счетчик
\ времени Pentium (который подсчитывает такты процессора).  Поскольку
\ TSC имеет ширину 64 бит мы должны push-ить его в стек в два приема

: RDTSC ( -- lsb msb )
    RDTSC    \ записывает результат в %edx:%eax
    EAX PUSH \ push lsb
    EDX PUSH \ push msb
;CODE

INLINE может использоваться для встраивания примитива ассемблера в текущее (ассемблерное) слово.

Например:

: 2DROP INLINE DROP INLINE DROP ;CODE

построит эффективное ассемблерное слово 2DROP, которое содержит встроенный код ассемблерной команды для DROP, за которым следует DROP (например, два 'pop %eax' инструкции в этом случае).

Другой пример. Рассмотрим это обычное определение Forth:

: C@++ ( addr -- addr+1 byte ) DUP 1+ SWAP C@ ;

(это эквивалентно операции Си '*p++' где p - указатель на char). Если вы заметили, что все слова, используемые для определения C@++, на самом деле являются ассемблерными примитивами, то мы можем писать быстрейшее (но эквивалентное) определение:

: C@++ INLINE DUP INLINE 1+ INLINE SWAP INLINE C@ ;CODE

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

Для успешного использования INLINE необходимо выполнить несколько условий:

  • (1) В настоящее время вы должны определить слово ассемблера (т.е. : ... ;CODE).
  • (2) Слово, в котором вы находитесь, должно быть известно как ассемблерное слово. Если вы попытаетесь вставить слово Forth, вы получите сообщение об ошибке.
  • (3) Ассемблерный примитив должен быть позиционно-независимым и должен заканчиваться одним NEXT макросом.

Упражнения для читателя:

  • (a) Обобщите INLINE, чтобы он мог вставлять слова Forth при построении слов Forth.
  • (b) Дальнейшее обобщение INLINE, чтобы оно делало что-то разумное, когда вы пытаетесь встроить Forth в ассемблер и наоборот.

Реализация INLINE довольно проста. Мы находим слово в словаре, проверяем его как ассемблерное слово, а затем копируем его в текущее определение байтом за байтом, пока не достигнем макроса NEXT (который не копируем).

HEX
: =NEXT ( addr -- next? )
    DUP C@ AD <> IF DROP FALSE EXIT THEN
    1+ DUP C@ FF <> IF DROP FALSE EXIT THEN
    1+     C@ 20 <> IF      FALSE EXIT THEN
    TRUE
;
DECIMAL

(  (INLINE) is the lowlevel inline function. )
:  (INLINE) ( cfa -- )
    @ ( remember codeword points to the code )
    BEGIN ( copy bytes until we hit NEXT macro )
        DUP =NEXT NOT
    WHILE
            DUP C@ C,
            1+
    REPEAT
    DROP
;

: INLINE IMMEDIATE
    WORD FIND ( find the word in the dictionary )
    >CFA ( codeword )

    DUP @ DOCOL = IF ( check codeword <> DOCOL  (ie. not a Forth word) )
        ." Cannot INLINE Forth words" CR ABORT
    THEN

    (INLINE)
;

HIDE =NEXT

Загадочный DOES

Концепция DOES> выглядит наиболее непонятной и даже мистической в Forth. DOES> так же один из наиболее мощных механизмов Forth, который в большинстве случаев заменяет объектно-ориентированное программирование. Действие и мощность DOES> основаны на codeword.

forth-interpret-40.png

Мы можем рассматривать codeword и param-field (поле параметров), которое идет за ним, под разными углами:

  • codeword – это "действие" производимое этим Forth-словом, а param-field – это данные, над которыми выполняется данное действие
  • codeword - это вызов подпрограммы, а param-field - это параметры (это может быть том числе инлайновый код) размещенные после CALL. Так может смотреть на эти вещи программист на ассемблере.
  • codeword - это единственный "метод" для этого "класса" слов, а param-field содержит "переменные экземпляра" для этого конкретного слова. Так это выглядит с точки зрения ООП программиста.

Общие особенности проявляются во всех этих точках зрения:

  • codeword всегда вызывается с как минимум одним аргументом, а именно, адресом param-field того слова, которое в данный момент исполняется. Этот param-field может содержать любое количество параметров.
  • Имеется сравнительно немного индивидуальных действий, на которые ссылается codeword. Каждое из этих действий широко распространено (за исключением низкоуровневых слов).
  • Интерпретация param-field полностью определяется содержимым codeword, то есть, каждый codeword ожидает, что param-field содержит определенный вид данных.

Типичное Forth-ядро изначально содержит некоторое количество подпрограмм, на которые ссылаются codeword-ы слов.

codeword Содержимое param-field
DOCOL последовательность интерпретируемых токенов
DOCON значение константы
DOVAR массив для произвольного набора данных
DOVOC информация словаря (варьируется в зависимости от реализации)

Наш Forth пока содержит только одну codeword-подпрограмму, которую мы назвали DOCOL. Она воспринимает содержимое своего param-field, как последовательность адресов слов, которые должны быть по очереди вызваны.

Forth-программа не ограничена приведенным набором codeword-подпрограмм, это свойство делает Forth очень мощным. Программист может определять новые codeword-подпрограммы, и новые соответствующие param-field-ы.

В терминах объектно-ориентированной парадигмы: новые "классы" и "методы" могут быть созданы, хотя каждый класс имеет только один метод.

(Прим.переводчика: это не совсем так, в некоторых Forth-системах есть VALUE переменные - у них три метода, есть варианты решения слов с произвольным количеством codeword-ов)

И подобно всем Forth-словам, codeword могут быть определены как на ассемблере, так и на уровне слов, определенных через двоеточие.

Теперь, когда мы освежили эти вещи в памяти, воспользуемся примером Брета Родригеза (Brad Rodriguez) для объяснения того, как работает DOES>. Мы ранее определили CONSTANT, который работает с использованием LIT. Пока забудем об этом и попробуем сделать все иначе. Давайте определим несколько французких числительных:

1 CONSTANT UN
2 CONSTANT DEUX
3 CONSTANT TROIS

Мы бы хотели, чтобы исполнение слова UN положило значение 1 на вершину стека данных. Исполнение DEUX – положило 2 и так далее.

CONSTANT в этом примере является определяющим словом: оно создает новое слово в словаре Fort-системы. Мы создали три слова-константы: UN, DEUX, и TROIS (вы можете считать это "экземплярами класса" CONSTANT). Каждое из трех слов будет иметь собственные поля кода, указывающий на один и тот же фрагмент машинного кода, который выполняет действие слова CONSTANT. Посмотрим, как это выглядит в памяти:

forth-interpret-41.png

Обратите внимание, что codeword всех трех слов указывает на некоторый машинный код, который должен push-нуть содержимое первого поля param-field, т.е. число: 1, 2 или 3 на стек.

Для написания этого машинного кода необходимо знать, как найти начало param-field. Вспомним, как у нас реализован NEXT:

.macro NEXT
    lodsl
    jmp *(%eax)
.endm

Регистр %ESI - это наш указатель на следующую выполняемую инструкцию. Команда LODSL загружает в регистр %EAX значение, лежащее по этому указателю и увеличивает %ESI на размер загруженных данных. А следующая команда JMP, осуществляет переход на значение, которое лежит по адресу, содержащемуся в %EAX.

Предположим, что мы находимся в вызывающем высокоуровневом коде

... SWAP DEUX + ...

с указателем %ESI на инструкцию DEUX. Мы заканчиваем выполнять инструкцию SWAP и в данный момент выполняем ее окончание - NEXT. Мы только что выполнили команду LODSL из NEXT и теперь ситуация такая, как на рисунке ниже.

forth-interpret-42.png

%ESI, только что указывал указывал на ячейку памяти, содержающую адрес codeword слова DEUX. Теперь он указывает следующую ячейку, как показано стрелкой, помеченной %esi (after lodsl).

В этот момент в регистре %EAX уже лежит адрес codeword слова DEUX. И сейчас JMP возьмет этот адрес из codeword и перейдет по нему, попадая в машинный код, помеченный ???. В этот момент, в регистре %EAX останется адрес, указывающий на codeword DEUX. И чтобы получить адрес начала param-field достаточно просто увеличить его на размер указателя (4 байта для нашей архитектуры), перепрыгивая через codeword.

В результате машинный код теперь знает, где лежат данные, с которыми ему нужно работать.

Получается, что для того чтобы положить "2" на стек, фрагмент машинного кода должен только:

  • увеличить на 4 регистр %EAX
  • взять значение по адресу %EAX
  • push-нуть его на стек
  • сделать NEXT, чтобы вернуться к выполнению  + 

Этот фрагмент кода часто называется DOCON. Полагаю, имеется ввиду "DO CONSTANT":

DOCON:
    lea     4(%eax), %eax
    movl    (%eax), %eax
    pushl   %eax
    NEXT

Теперь нам нужно ответить на 3 важных вопроса:

  • (a) Как мы должны создавать новые Forth-слова, которые содержат некоторые произвольные данные в поле параметров?
  • (b) Как мы будем изменять codeword этого слова, чтобы указать на некоторый используемый нами машинный код?
  • (c) Как (и куда) мы будем компилировать этот фрагмент машинного кода, который изолирован от использующих его слов?

Ответ на пункт (а): мы пишем Forth-слова для реализации этого. В Forth для этого существуют так называемые "определяющие слова", которые во время исполнения могут создавать другие слова. CONSTANT, что мы определили в этом разделе - это один из примеров определяющих слов. Всю работу определяющего слова выполняет слово CREATE, которое берет из входного потока имя слова, создает заголовок слова и codeword для нового слова, и привязывает все это в словарь. Программисту остается создать param-field.

Ответ на (б) и (с) воплощен в два слова, называемые (;CODE) и ;CODE соответственно. Для того, чтобы понять как они работают, давайте глянем на определяющее слово CONSTANT теперь написанное на Форте и использованием Forth-ассемблера, которого у нас еще нет. Даже если мы не будем писать Forth-ассемблер, это обсуждение пригодится нам для понимания идеи.

: CONSTANT ( n -- )
    CREATE        \ создать новое слово
    DOCOL ,       \ добавить DOCOL как codeword поля слова
    ,             \ компилировать верхнее значение со стека данных
                  \ как первый (и единственный) параметр param-field
    ;CODE         \ завершить высокоуровневый код и начать низкоуровневый

    LEA   4(%EAX), %EAX  \ фрагмент машинного кода для DOCON
    MOV   (%EAX), %EAX
    PUSH  %EAX
    NEXT

END-CODE          \ завершить определение

В этом примере Forth слово состоит из двух частей.

  • Все от : CONSTANT до ;CODE - высокоуровневый Forth-код, исполняемый при вызове слова CONSTANT.
  • Все от ;CODE до END-CODE - это машинный код, исполняемый, когда "потомок" слова CONSTANT (такой как UN и DEUX) исполняется. То есть, все начиная с ;CODE до END-CODE – это фрагмент машинного кода, на который будут указывать все слова константы, определенные чезез CONSTANT. ;CODE означает что высокоуровневая часть слова закончилась (";") и начинается определение в машинном коде. В любом случае это НЕ означает, что в словаре будет создано два отдельных имени. Все, начиная с : CONSTANT до END-CODE, содержится в param-field слова:

forth-interpret-43.png

Разделим этапы выполнения на три "последовательности", которые позволяют понять работу определяющих слов:

  • Первая последовательность, когда родительское слово компилируется. Это включает и высокоуровневую часть определения и ассемблерную, то есть момент включения слова CONSTANT в словарь. Как мы дальше увидим, ;CODE - это директива компилятора, исполняемая во время определения первой последовательности.
  • Вторая последовательность, когда родительское слово исполняется, а дочернее слово компилируется то есть, когда в словаре создается (дочернее) слово CONSTANT-класса. В примере 2 CONSTANT DEUX вторая последовательность начинается во время исполнения слова CONSTANT, и слово DEUX добавляется в словарь. Во время второй последовательности выскоуровневая часть CONSTANT исполняется, в том числе слово (;CODE).
  • Третья последовательность, когда дочернее слово исполняется . В нашем примере, третья последовательность выполняется, когда DEUX исполняется чтобы push-нуть значение 2 на стек данных. То есть это время исполнения куска машинного кода слова CONSTANT.

Слова ;CODE и (;CODE) делают следующее:

  • ;CODE исполняется во время первой последовательности, то есть во время компиляции CONSTANT. Это пример Forth-слова немедленного исполнения – слово исполняется во время компиляции Forth-кода. ;CODE делает три вещи:
    • (a) компилирует в код определяемого CONSTANT слово (;CODE)
    • (b) выключает режим компиляции
    • (c) запускает Forth-ассемблер.
  • (;CODE) – это часть слова CONSTANT, поэтому оно исполняется во время второй последовательности, то есть во время исполнения слова CONSTANT. Оно выполняет следующие действия:
    • (a) возвращает адрес машинного кода, который следует сразу за ним. Это выполняется за счет pop-а адреса со стека возвратов.
    • (b) компилирует этот адрес в codeword только что определенного (с помощью CREATE) слова. LATEST возвращает адрес codeword этого слова.
    • (c) выполняет действие слова EXIT так, чтобы интерпретатор Forth не пытался выполнить этот машинный код. Это высокоуровневый "выход из подпрограммы", который завершает Forth-определение.

Вот пример реализации:

: ;CODE
    [COMPILE] (;CODE)   \ компилировать код (;CODE) в определение
    ?CSP [COMPILE] [    \ выключить режим компиляции
    REVEAL              \ выполняет действие, аналогичное ‘;’
    ASSEMBLER           \ включить ассемблер
; IMMEDIATE          \ Это слово немедленного исполнения!
: (;CODE)
    R>                  \ выталкивает адрес машинного кода со стека возвратов
    LATEST @ NAME>      \ берет адрес codeword последнего слова
    !                   \ сохраняет адрес машинного кода в codeword создаваемого слова
;

Из них более необычный - это (;CODE). Поскольку это высокоуровневое определение, адрес, на который произойдет переход после завершения CONSTANT - высокоуровневый адрес возврата - push-ится на стек возвратов. Поэтому, выталкивание из стека возвратов изнутри (;CODE) приведет к получению адреса машинного кода. Кроме того, выемка этого значения из стека возвратов будет "обходить" один уровень подпрограммного выхода, таким образом, что когда (;CODE) выйдет, это будет выход в слово вызывающее CONSTANT. Это эквивалентно возврату в CONSTANT и затем сразу выходу из CONSTANT. Проследите исполнение слов CONSTANT и (;CODE) на рисунке, чтобы разобраться в их работе.

Выразим то же самое на ассемблере - это копия определения, размещенного в предыдущем файле.

defcode "(;CODE)",7,,SUBCODE
    POPRSP  %eax            # pop со стека возвратов в %eax
    mov     var_LATEST, %ebx # берем адрес codeword последнего слова
    mov     %eax, (%ebx)    # сохраняем адрес машинного кода в codeword последнего слова
    NEXT

Мы уже рассмотрели, как заставить Forth-слово исполнить выбранный фрагмент машинного кода и как передать этому фрагменту кода адрес param field слова. Но как можно написать этот машинный код на высокоуровневом Forth?

Каждое Forth-слово должно (с помощью NEXT) исполнять некоторый машинный код. Для этого и существует codeword. Поэтому, подпрограмма машинного кода (или набор таковых) должна решать проблемы извлечения высокоуровневых действий. Мы называем эту подпрограмму DODOES. При этом должны быть разрешены три проблемы:

  • (a) как найти адрес высокоуровневого кода, ассоциируемого с этим Forth-словом?
  • (b) как мы будем (из машинного кода) вызывать Forth-интерпретатор для высокоуровневой подпрограммы действия?
  • (c) Как мы будем передавать этой подпрограмме адрес param-field для исполняемого в этот момент слова?

Ответ на (с) – как передавать аргумент в высокоуровневое Forth-слово - прост. На стеке данных, конечно же. Наша машинная подпрограмма должна push-ить адрес param-field на стек перед тем, как вызвать высокоуровневый код (из нашей предыдущей работы мы знаем, как подпрограмма в машинном коде может получить адрес поля параметров).

Ответ на (b) несколько сложнее. Обычно, мы хотим делать что-то похожее на Forth-слово EXECUTE, которое вызывает Forth-слово или, возможно, DOCOL, который вызывает двоеточное определение. Оба относятся к числу наших ключевых слов. DODOES будет иметь с ними сходство.

Вопрос (a) самый сложный. Куда поместить адрес высокоуровневой подпрограммы? Вспомните, codeword НЕ указывает на высокоуровневый код, оно должно указывать на машинный код. Два подхода использовались ранее:

  • Fig-Forth решение. Fig-Forth резервирует первую ячейку в param-field для хранения адреса высокоуровневого кода. DODOES впоследствии извлекает адрес param-field, push-ит адрес реальных данных (обычно следующих за первой ячейкой) на стек данных, извлекает адрес высокоуровневой подпрограммы и исполняет ее.

С этим решением связаны две проблемы. Во-первых, структура поля параметров различна в низкоуровневых и высокоуровневых словах. К примеру, CONSTANT, будучи определено в машинном коде, будет хранить свои данные в param-field, в то время, как оно же, определенное на высокоуровневом коде будет хранить свои данные, обычно, по адресу равному param-field + 1ячейка. Во-вторых, каждое объявление высокоуровневого действия приводит к дополнительному расходу одной ячейки памяти. То есть, если CONSTANT использует высокоуровневое действие, каждая вновь созданная в программе константа будет больше на одну ячейку!

К счастью, хорошие Forth-программисты быстро изобрели решение, которое побороло эти проблемы, и решение fig-Forth было забыто.

  • Современное решение. Большинство Фортов сегодня объединяет различные фрагменты машинного кода с каждой высокоуровневой процедурой действия. Поэтому, высокоуровневые константы будут иметь собственный codeword, указывающий на фрагмент машинного кода, чья единственная функция - вызвать высокоуровневое действие CONSTANT. codeword переменной указывает на процедуру "запуска" для высокоуровневого действия VARIABLE и.т.п.

Чрезмерное ли это повторение кода? Нет, потому что каждый такой фрагмент машинного кода лишь подпрограммный вызов на обычную общую подпрограмму DODOES (Это отлично от fig-Forth подпрограммы DODOES). Адрес высокоуровневого кода в DODOES передается как "инлайновый" параметр подпрограммы. То есть, адрес высокоуровневого кода кладется сразу после CALL инструкции. DODOES может затем pop-нуть адрес с процессорного стека и произвести чтение, чтобы получить этот адрес.

Фактически, мы делаем еще два упрощения. Высокоуровневый код расположен сразу за инструкцией CALL. Поэтому DODOES может извлечь этот адрес прямо с процессорного стека. И поскольку мы знаем, что это высокоуровневый Forth-код, мы работаем с его codeword и просто компилируем высокоуровневый код, по-существу, встраивая действие DOCOL в DODOES.

Теперь, каждое дочернее слово просто указывает на кусочек машинного кода, а в его param-field место не расходуется. Этот кусочек машинного кода - CALL инструкция ведущая на процедуру DODOES, за которой расположен высокоуровневый код.

Это, несомненно, наиболее закрученная программная логика во всем ядре Forth! Так давайте посмотрим, как это реализуется на практике.

forth-interpret-44.png

Когда адресный интерпретатор встречает DEUX (то есть, когда %ESI указывает на DEUX в верхнем левом углу) он выполняет обычную вещь: извлекает адрес, хранящийся в codeword DEUX, и передает на него управление. По этому адресу находится инструкция CALL DODOES, поэтому второй переход (в этот раз подпрограммный вызов) производится сразу. DODOES затем должен произвести следующие действия:

  • (a) Push-нуть адрес param-field слова DEUX на стек данных для последующего использования в высокоуровневой подпрограмме. Инструкция CALL в момент своего выполнения не изменяет никаких регистров, поэтому мы ожидаем обнаружить адрес param-field слова DEUX, вычислив %EAX+4.
  • (b) Добыть адрес высокоуровневой подпрограммы, за счет выталкивания из стека CPU. Это адрес высокоуровневого кода, то есть param-field двоеточного определения.
  • (c) сохранить старое значение указателя интерпретации (%esi after lodsl) на стеке возвратов. C этого момента регистр %ESI будет использоваться при исполнении высокоуровневого фрагмента кода. По существу, DODOES должен использовать %ESI, подобно тому, как это делает DOCOL. Помните, что стек возвратов Forth - это не стек CPU.
  • (d) положить адрес высокоуровневого слова в %ESI (это %ESI(dodoes) на рисунке)
  • (e) выполнить NEXT для продолжения интерпретации выскоуровневого кода с нового места.

DODOES может быть написан следующим образом:

DODOES:
    PUSHRSP %esi            ;; (с) Сохраняем ESI на стеке возвратов

    pop     %esi            ;; (b,d) CALL-RETADDR -> ESI

    lea     4(%eax), %eax   ;; (a) вычислить param-field DEUX
    pushl   %eax            ;; (a) push его на стек данных

    NEXT                    ;; (e) вызвать интерпретатор

Эти операции идут немного в другом порядке, потому что мы используем стек CPU как стек данных. Но пока правильные данные уходят на правильные стеки (или в правильные регистры) в правильное время, точная последовательность операций не критична. В этом случае мы учитываем, что старое значение %ESI должно быть push-нуто на стек возвратов перед извлечением нового %ESP из стека CPU.

Использование DOES

Мы изучили, как создавать новое Forth-слово с помощью ;CODE, хранящее произвольные данные в поле параметров, и как менять указатель в поле кода на новый фрагмент машинного кода. Как можно компилировать высокоуровневые слова, и делать так, чтобы новое слово ссылалось на него?

Ответ содержится в двух словах DOES> и (DOES>), которые являются высокоуровневым эквивалентом слов ;CODE и (;CODE). Чтобы их понять, давайте посмотрим на пример их использования:

: CONSTANT ( n -- )
    CREATE   \ создать новое слово
    DOCOL ,  \ добавить DOCOL как codeword поля слова
    ,        \ добавить значение с вершины стека данных
             \ в текущее определение как первое значение в
             \ поле параметров созданного слова
  DOES>      \ завершение «создающей» части, начало части «действия»
    @        \ прочесть значение из поля параметров слова,
             \ разыменовать для получения содержимого
;

Сравните это с предыдущим примером ;CODE и заметьте, что DOES> выполняет функцию, аналогичную ;CODE. Все от CONSTANT до DOES> исполняется когда слово CONSTANT вызывается. Это код, который формирует поле параметров определяемого слова. Все от DOES> до ; - высокоуровневый код, исполняемый когда "потомок" CONSTANT (к примеру, DEUX) вызывается, т.е. высокоуровневый фрагмент кода, на который указывает codeword (мы увидим, что CALL DODOES включено перед этим высокоуровневым фрагментом). Так как с ;CODE оба класса: порождающий и действия содержатся внутри тела Forth-слова CONSTANT, как показано на рисунке:

forth-interpret-45.png

Пересмотрите последовательности о которых мы говорили. Слова DOES> и (DOES>) делают следующее:

DOES> исполняется в первой последовательности, когда компилируется CONSTANT. Таким образом DOES> - это Forth-слово немедленного исполнения, оно делает следующие две вещи:

  • (a) компилирует Forth-слово (DOES>) в CONSTANT.
  • (b) компилирует CALL DODOES в CONSTANT.

Замечу, что DOES> оставляет Forth-компилятор включенным, для последующей компиляции высокоуровневого фрагмента, следующего за ним. Так же, даже если CALL DODOES не является Forth-кодом, слова немедленного исполнения, такие как DOES> могут компилироваться в середину Forth-определения.

(DOES>) является частью слова CONSTANT, поэтому она исполняется, когда CONSTANT исполняется (вторая последовательность). Оно делает следующее:

  • (a) получает адрес машинного кода, который следует сразу за CALL DODOES, с помощью выталкивания IP со стека возвратов Forth
  • (b) этот адрес записывается в codeword только что определенного с помощью CREATE слова.
  • (c) выполняется действие EXIT, заставляющее CONSTANT завершить выполнение не допуская исполнения следующего фрагмента кода (который выполняется в момент вызова созданной константы).

Как видим, действие (DOES>) идентично (;CODE), поэтому отдельное слово не обязательно. Я буду использовать (;CODE) с этого момента вместо (DOES>).

Мы уже определили (;CODE). Определение DOES>:

: DOES>
    [COMPILE] (;CODE)   \ компилирует (;CODE) в определение
    0E8 C,              \ байт опкода CALL
    DODOES HERE 4+ - ,  \ относительное смещение к DODOES
; IMMEDIATE

где DODOES - константа, которая хранит адрес подпрограммы DODOES. В случае i386 инструкция CALL ожидает относительный адреc - отсюда арифметика использующая DODOES и HERE.

Кто мог подумать, что несколько линий кода потребуют такого количества пояснений? Именно поэтому я восхищаюсь ;CODE и DOES> так сильно… Я никогда ранее не видел таких запутанных, мощных и гибких конструкций, закодированных с подобной экономией.

Реализация DOES

Richard WM Jones говорит, что: "DOES> невозможно реализовать с помощью этого Forth, потому что у нас нет отдельного указателя данных.". Однако, есть другой взгляд на это: http://osdevnotes.blogspot.ru/2015/07/does-in-jonesforth.html

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

Давайте сначала посмотрим, как DOES> используется.

: MKCON
    WORD           \ прочтем слово с stdin
    CREATE         \ создадим заголовок слова
    0              \ положим в стек заглушку для codeword, которая будет перезаписана DOES>
    ,              \ скомпилируем заглушку
    ,              \ скомпилируем константу со стека, которая была положена на него
                   \ до вызова MKCON
  DOES>
    @              \ Разименовать
;

Это создает слово MKCON, которое при вызове:

1337 MKCON PUSH1337

…создает новое слово PUSH1337, которое будет вести себя, как если бы оно было определено как:

: PUSH1337 1337 ;

Вспомните [CREATE…;CODE example]. DOES> очень похож на ;CODE, за исключением того, что вы хотите использовать слова Forth, а не встроенные машинные слова. В ;CODE, встроенные машинные слова встраиваются в слово, используя CREATE...;CODE, и в CREATE...DOES> вместо этого это будут Forth-слова. Так что если бы у нас не было DOES>-слова, мы могли бы написать что-то вроде:

: MKCON WORD CREATE 0 , , ;CODE $DODOES @ ;

…где $DODOES - это слово генерирующее машинный код, которое создает волшебство, которое мы еще не выяснили. $DODOES должно вести себя как смесь между DOCOL and NEXT, который регулирует FIP (указатель инструкции коссвенного шитого кода, указывающий на следующее слово для выполнения), чтобы тот указывал на прошлый $DODOES для слова @. The DFA (param-field) созданного слова (то есть слова PUSH1337) помещается в стек, так @ может прочесть константу (1337) снаружи. Это означает, что самый простой CREATE...DOES> пример:

: DUMMY WORD CREATE 0 , DOES> DROP ;
DUMMY ADUMMY

…потому что нам нужно очистить DFA для ADUMMY который push-ится, когда он вызывается. В любом случае, мы могли бы таким образом определить DOES>:

: DOES> IMMEDIATE ' (;CODE) , [COMPILE] $DODOES ;

Давайте рассмотрим два способа реализации $DODOES. Путь 1 - полностью заинлайненный. Адрес Forth-слов (новый FIP) вычисляется путем пропускания бит, испускаемых $DODOES.

.macro COMPILE_INSN, insn:vararg
    .int LIT
    \insn
    .int COMMA
.endm

.macro NEXT_BODY, wrap_insn:vararg=
    \wrap_insn ldr r0, [FIP], #4
    \wrap_insn ldr r1, [r0]
    \wrap_insn bx  r1
.endm
    /*
     A CREATE...DOES> слово в основном является специальным CREATE... ;CODE
     словом, где Forth-слова идут за $DODOES. $DODOES таким образом
     настраивает FIP чтобы они указывали за $DODOES, а потом делает NEXT.

     Вы можете думать об этом как о специальном DOCOL который устанавливает FIP
     на определенное смещение в CREATE...DOES> word's DFA. Эта
     версия встроена в DFA так что найти FIP также легко как
     перемещение прошлого FIP.

     - Just like DOCOL, we enter with CFA in r0. (у нас это EAX)
     - Just like DOCOL, we need to push (old) FIP for EXIT to pop.
     - The forth words expect DFA on stack.
    */
.macro DODOES_BODY, magic=, wrap_insn:vararg=
0:  \wrap_insn PUSHRSP FIP
1:  \wrap_insn ldr FIP, [r0]
    \wrap_insn add FIP, FIP, #((2f-0b)/((1b-0b)/(4)))
    \wrap_insn add r0, r0, #4
    \wrap_insn PUSHDSP r0
    NEXT_BODY \wrap_insn
2:
.endm
    /*
    @ $DODOES ( -- ) emits the machine words used by DOES>.
    */
defword "$DODOES",F_IMM,ASMDODOES
    DODOES_BODY ASMDODOES, COMPILE_INSN
    .int EXIT

Путь 2 - частично инлайновый, где испускаемый код делает абсолютную branch и link. Это уменьшает объем памяти, используемой для определения, за счет ветки. В конечном итоге это решение и было принято. _DODOES вычисляет новый FIP, настраивающий адрес возврата из ветки и ссылки, выполняемой заинлайненными битами.

_DODOES:
    PUSHRSP FIP        @ just like DOCOL, for EXIT to work
    mov FIP, lr        @ FIP now points to label 3 below
    add FIP, FIP, #4   @ add 4 to skip past ldr storage
    add r0, r0, #4     @ r0 was CFA
    PUSHDSP r0         @ need to push DFA onto stack
    NEXT

.macro DODOES_BODY, wrap_insn:vararg=
1:      \wrap_insn ldr r12, . + ((3f-1b)/((2f-1b)/(4)))
2:      \wrap_insn blx r12
3:      \wrap_insn .long _DODOES
.endm

@
@ $DODOES ( -- ) emits the machine words used by DOES>.
@
defword "$DODOES",F_IMM,ASMDODOES
    DODOES_BODY COMPILE_INSN
    .int EXIT

В любом случае, как и DOCOL, нам нужно push-нуть старый указатель FIP перед вычислением нового. Старый указатель FIP соответствует адресу внутри слова, которое называется DOES>-созданное слово. В обоих случаях нам нужно push-ить DFA исполняемого слова на стек (это находится в r0 на AArch32 Jonesforth).

В конце концов, в обоих случаях CREATE...DOES> слово неотличимо от CREATE...;CODE слова, и созданное слово неотличимо от слова, созданного с помощью CREATE...;CODE.

\ This is the CREATE...;CODE $DOCON END-CODE example before.
: MKCON WORD CREATE 0 , , ;CODE ( MKCON+7 ) E590C004 E52DC004 E49A0004 E5901000 E12FFF11 (END-CODE)
CODE CON ( CODEWORD MKCON+7 ) 5 (END-CODE)

\ Fully inlined CREATE...DOES>.
: MKCON_WAY1 WORD CREATE 0 , , ;CODE ( MKCON_WAY1+7) E52BA004 E590A000 E28AA020 E2800004 E52D0004 E49A0004 E5901000 E12FFF11 9714 938C (END-CODE)
CODE CON_BY_WAY1 ( CODEWORD MKCON_WAY1+7 ) 5 (END-CODE)

\ Partly-inlined CREATE...DOES>.
: MKCON_WAY2 WORD CREATE 0 , , ;CODE ( MKCON_WAY2+7 ) E59FC000 E12FFF3C 9F64 9714 938C (END-CODE)
CODE CON_BY_WAY2 ( CODEWORD MKCON_WAY2+7 ) 5 (END-CODE)

Это делает декомпиляцию (т.е. использование слова SEE) немного сложной, но не невозможной. Как вы можете видеть здесь, я еще не написал хорошего дизассемблера, который бы обнаружил эти последовательности как $DOCON. ИМХО, это все еще меньшее зло, чем введение новых полей или флагов в заголовок определения слова.

P.S. Определение констант - это классический пример использования DOES>, но немного глупый, когда применяется к Jonesforth, где это интринсик. Это интринсик, потому что некоторые константы времени компиляции, известные только в момент ассемблера, могут быть видимы во время рабоы Forth-системы. Другой классический пример DOES> это определения структур.

P.P.S. Вам может быть интересно, как я смотрю на кодовые слова, так как ни Jonesforth, ни PijFORTH не поддерживают его. Думаю, я буду вести блог об этом в ближайшее время, когда … (CODEWORD XXX) здесь показывает codeword, на которой указывает CFA, что обязательно не DOCOL (иначе это было бы регулярное определение двоеточия, а не КОД). Обозначение слова (CODEWORD word+offset) указывает вам, что машинные слова, на которые указывает CFA, являются частью другого слова. Родные (jonesforth.s-defined) интрисинки будут декомпилироваться как-то вроде:

CODE 2SWAP ( CODEWORD 85BC ) (END-CODE)

Приветствие

Это слово печатает версию и "ok":

: WELCOME
    S" TEST-MODE" FIND NOT IF
        ." JONESFORTH VERSION " VERSION . CR
        \ UNUSED .
        \ ." CELLS REMAINING" CR
        ." OK "
    THEN
;
WELCOME
HIDE WELCOME

Tangling

<<forth_divmod>>

<<forth_symbol_constants>>

<<forth_negate>>

<<forth_booleans>>

<<forth_literal>>

<<forth_literal_colon>>

<<forth_literal_others>>

<<forth_compile>>

<<forth_recurse>>

<<forth_if>>

<<forth_begin_until>>

<<forth_again>>

<<forth_while_repeat>>

<<forth_unless>>

<<forth_parens>>

<<forth_nip_tuck_pick_spaces_decimal_hex>>

<<forth_u_print>>

<<forth_stack_print>>

<<forth_uwidth_udotr>>

<<forth_dotr>>

<<forth_dotr_with_trailing>>

<<forth_udot_with_trailing>>

<<forth_enigma>>

<<forth_within>>

<<forth_depth>>

<<forth_aligned>>

<<forth_align>>

<<forth_strings>>

<<forth_dotstring>>

<<forth_constant>>

<<forth_allot>>

<<forth_cells>>

<<forth_variable>>

<<forth_to>>

<<forth_to_plus>>

<<forth_id_dot>>

<<forth_hidden_immediate_question>>

<<forth_words>>

<<forth_forget>>

<<forth_dump>>

<<forth_case>>

<<forth_cfa>>

<<forth_see>>

<<forth_noname>>

<<forth_exceptions>>

<<forth_zerostrings>>

<<forth_argc>>

<<forth_argv>>

<<forth_environ>>

<<forth_bye>>

<<forth_unused>>

<<forth_morecore>>

<<forth_files>>

<<forth_asm>>

<<forth_inlining_asm>>

<<forth_welcome>>

<<forth_all>>
Яндекс.Метрика
Home