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, т.е. заносит указатель на codeword слова word по адресу HERE, а потом увеличивает HERE на длину указателя.

В IMMEDIATE-режиме он просто "немедленно" исполняет word. [TODO:gmm] Рассмотреть почему.

: [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-ассемблер

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

Сначала мы рассмотрим способ, который позволяет написать и скомпилировать Forth-слово как ассемблерный примитив. Такое слово будем начинать обычным способом: с двоеточия и имени слова, но заканчивать не точкой с запятой а словом ;ASMCODE.

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

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

Cлово ;ASMCODE во время своего выполнения должно сделать две очень важные вещи: добавить к определяемому слову код NEXT и исправить codeword таким образом, чтобы он указывал не на DOCOL, а на на только что скомпилированный машинный код. Этот скомпилированный код размещен сразу за codeword и таким образом результат ничем не будет отличаться от любых других ассемблерных примитивов.

Кроме того, ;ASMCODE делает другие вещи, которые при определении высокоуровневых примитивов делает ;:

  • переключение HIDDEN-флаг
  • возврат в IMMEDIATE-режим

Все они подробно описаны в комментариях.

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

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

HEX

<<forth_next>>

<<forth_semi_asmcode>>

\ Регистры и соответтсвующие им значения битов reg
: EAX IMMEDIATE 0 ; \ 000
: ECX IMMEDIATE 1 ; \ 001
: EDX IMMEDIATE 2 ; \ 010
: EBX IMMEDIATE 3 ; \ 011
: ESP IMMEDIATE 4 ; \ 100
: EBP IMMEDIATE 5 ; \ 101
: ESI IMMEDIATE 6 ; \ 110
: EDI IMMEDIATE 7 ; \ 111

: AL IMMEDIATE 0 ; \ 000
: CL IMMEDIATE 1 ; \ 001
: DL IMMEDIATE 2 ; \ 010
: BL IMMEDIATE 3 ; \ 011
: AH IMMEDIATE 4 ; \ 100
: CH IMMEDIATE 5 ; \ 101
: DH IMMEDIATE 6 ; \ 110
: BH IMMEDIATE 7 ; \ 111

\ Стековые инструкции
: 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
;ASMCODE

Режимы адресации

Определение режимов адресации - байт modr/m.

forth-interpret-modrm.png

Значение этого байта определяет используемую форму адреса операндов. Операнды могут находиться в памяти, в одном, или двух регистрах. Если операнд находится в памяти, то байт modr/m определяет компоненты (смещение, базовый и индексный регистры), используемые для вычисления его эффективного адреса. В защищенном режиме (это наш случай) для определения местоположения операнда в памяти может дополнительно использоваться байт SIB (Scale-Index-Base – масштаб-индекс-база). Байт modr/m состоит из трех битовых полей:

  • поле mod (биты 7 и 6) - определяет количество байт, занимаемых в команде адресом операнда. Поле mod используется совместно с полем r/m, которое указывает способ модификации адреса операнда смещения в команде.
    • Если mod = 00 (MOD-DISP-NONE), это означает, что поле смещения в команде отсутствует, и адрес операнда определяется содержимым базового и (или) индексного регистра. Какие именно регистры будут использоваться для вычисления эффективного адреса, определяется значением оставшихся битов байта modr/m.
    • Если mod = 01 (MOD-DISP-SHORT), это означает, что поле "смещение" в команде присутствует, занимает 1 байт и модифицируется содержимым базового и (или) индексного регистра.
    • Если mod = 10 (MOD-DISP), это означает, что поле смещение в команде присутствует, занимает 2 или 4 байта (в зависимости от действующего по умолчанию или определяемого префиксом размера адреса) и модифицируется содержимым базового и (или) индексного регистра.
    • Если mod = 11 (MOD-REG-OR-IMM), это означает, что операндов в памяти нет: они находятся в регистрах. Это же значение mod используется в случае, когда в команде применяется непосредственный операнд;
  • поле reg (биты 5,4,3) определяет либо регистр, находящийся в команде на месте операнда-приемника (destination), либо возможное расширение кода операции.
  • поле r/m используется совместно с полем mod и определяет либо регистр, находящийся в команде на месте первого операнда (если mod = 11), либо используемые для вычисления эффективного адреса (совместно с полем смещения в команде) базовые и индексные регистры.

Если в команде участвуют два регистра, то поле reg определяет операнд-приемник, а поле r/m - источник.

HEX
: MOD-DISP-NONE    0  ; \ 00---+++
: MOD-DISP-SHORT   40 ; \ 01---+++
: MOD-DISP         80 ; \ 10---+++
: MOD-REG-OR-IMM   C0 ; \ 11---+++
: REG-DST ( --+++reg -- --reg000 )                                  8 * ;
: REG-SRC ( --+++reg -- --+++reg )                                      ;
: TWO-REG ( reg-dst reg-src -- ++regreg )   SWAP REG-DST SWAP REG-SRC + ;
: MODR/M  ( mod reg -- modr/m    )                                    + ;

Этого уже достаточно, чтобы ассемблировать команду LEA с регистровыми операндами:

: LEA IMMEDIATE
    8D C,
    TWO-REG MODR/M C,
    C,
;
04 MOD-DISP-SHORT EAX ECX LEA
=>
80523DC 8D 41 04

Или команду MOV, которая работает с базовой регистровой адресацией

: MOV-R32,R/M32 IMMEDIATE
    8B C,
    TWO-REG MODR/M C,
;
MOD-DISP-NONE EAX EAX MOV-R32,R/M32
=>
805247E 8B 00

Байт SIB

Байт масштаба, индекса и базы (Scale-Index-Base - SIB) используется для расширения возможностей адресации операндов. На наличие байта SIB в машинной команде указывает сочетание одного из значений 01 или 10 поля mod и значения поля r/m = 100. Байт sib состоит из трех элементов:

forth-interpret-sib.png

  • В поле масштаба ss размещается масштабный множитель для индексного компонента index, занимающего следующие три бита байта sib. В поле ss может содержаться значение 1, 2, 4 или 8. При вычислении эффективного адреса на это значение будет умножаться содержимое индексного регистра.
  • Поле index позволяет хранить номер индексного регистра, содержимое которого применяется для вычисления эффективного адреса операнда.
  • Поле base требуется для хранения номера базового регистра, содержимое которого также применяется для вычисления эффективного адреса операнда. В качестве базового и индексного регистров могут использоваться большинство регистров общего назначения.

Особый случай - значение поля base = 101 (что соответствует регистру EBP). Это означает наличие в команде адреса смещения disp32 без базы, если mod = 00 и [EBP] в противном случае. Такой подход обеспечивает следующие режимы адресации:

  • disp32[index], если mod = 00
  • disp8[ebp][index], если mod = 01
  • disp32[ebp][index], если mod = 10

Поля смещения и непосредственного операнда

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

Поле непосредственного операнда - необязательное поле, представляющее собой 8-, 16- или 32-разрядный непосредственный операнд. Наличие этого поля, конечно, отражается на значении байта modr/m.

Инлайнинг

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

Например:

: 2DROP INLINE DROP INLINE DROP ;ASMCODE

построит эффективное ассемблерное слово 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@ ;ASMCODE

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

  • (1) В настоящее время вы должны определять слово ассемблера (т.е. : ... ;ASMCODE).
  • (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

Создание определяющих слов

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

forth-interpret-30.png

Это позволяет интерпретировать param-field дочернего слова любым способом, что само по себе является чрезвычайно мощным методом.

Мы можем рассматривать 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 содержит определенный вид данных.

Для того, чтобы получить доступ к param-field дочернего слова, нам необходимо вспомнить реализацию NEXT:

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

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

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

... SWAP CHILD DUP ...

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

forth-interpret-31.png

%ESI, только что указывал указывал на ячейку памяти, содержающую адрес codeword слова CHILD. Это состояние у нас обозначено %esi(1). После выполнения LODSL он указывает следующую ячейку, как показано стрелкой, помеченной %esi(2).

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

В результате интерпретатор теперь знает, где лежат данные, с которыми ему нужно работать. По окончании своей работы интерпретатор должен выполнить NEXT, чтобы управление было передано слову DUP. Таким образом мы можем представить себе интерпретатор в виде такого кода:

lea     4(%eax), %eax   # находим param-field слова CHILD
# ... выполняем какие-то действия
NEXT                    # продолжаем выполнение

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

lea     4(%eax), %eax   # 8d 40 04  # находим param-field слова CHILD
movl    (%eax), %eax    # 8b 00     # копируем первый параметр из ~param-field~
pushl   %eax            # 50        # отправляем его в стек
NEXT                    # ad ff 20  # продолжаем выполнение

Для того, чтобы поместить этот интерпретатор в конец определяемого слова, поступим также, как мы поступали ранее, чтобы вкомпилить NEXT: напишем IMMEDIATE-слово, которое вкомпилит интерпретатор его байт за байтом, начиная с того места, куда указывает HERE.

HEX
: (DOCON) IMMEDIATE
      8D C, 40 C, 04 C,  \ lea     4(%eax), %eax
      8B C, 00 C,        \ movl    (%eax), %eax
      50 C,              \ pushl   %eax
      AD C, FF C, 20 C,  \ NEXT
;

Отлично, у нас есть интерпретатор в машинном коде, который мы поместим в конец определяющего слова. И мы можем двигаться дальше.

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

Нам нужно понимать, что родительское слово, как и любое другое, имеет 2 фазы работы.

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

Рассмотрим сначал вторую фазу. Когда родительское слово исполняется, чтобы скомпилировать дочернее слово, оно должно:

  • Получить из входного потока имя дочернего слова (с помощью WORD)
  • Создать слованый заголовок дочернего слова (используя CREATE)
  • Вкомпилировать в дочернее слово такой codeword, чтобы он указывал на интерпретатор, размещенный в родительском слове.
  • Сформировать param-field дочернего слова (например, взяв параметры из стека)
  • Выполнить EXIT, чтобы вернуть управление

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

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

Для того, чтобы вычислить адрес интерпретатора, на стадии, когда родительское слово компилируется мы ненадолго перейдем в immediate-режим. Выполнив расчет, мы вкомпилим результат как константу (за словом LIT) в родительское слово, что даст нам возможность использовать его, на стадии, когдаa родительское слово исполняется, чтобы настроить codeword дочернего слова.

Сейчас мы построим таким образом родительское слово DEFCONST, которое будучи вызванным как:

1337 DEFCONST PUSH1337

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

: PUSH1337 1337 ;
: DEFCONST
    WORD             \ прочтем слово с stdin
    CREATE           \ создадим заголовок слова
    0 ,              \ вместо codeword вкомпилим заглушку-ноль
    ,                \ скомпилируем param-field взяв его со стека (в нашем примере - 1337)
    [COMPILE] [      \ вкомпилить в DEFCONST переход в immediate-режим
    \ Здесь, во время определения слова DEFCONST мы можем
    \ вычислить начало ассемблерного кода, вкомпилив его адрес как литерал
    \ чтобы во время выполнения DEFCONST заменить codeword создаваемого
    \ дочернего слова на адрес машинного кода
    LIT
    [            \ Ненадолго переходим в IMMEDIATE-режим - compile-time вычисления
      HEX
      HERE @ 18 +    \ Вычисляем адрес начала машинного кода относительно HERE:
                     \ сейчас будет вкомпилен вычисленный адрес, потом
                     \ еще 5 команд, всего 6, каждая по 4 байта = 24
                     \ байта в десятичной = 18 в шестнадцатиричной.
      ,              \ И вкомпиливаем его в DEFCONST
    ]            \ Возврат из IMMEDIATE-режима
    LATEST @ >CFA    \ получаем CFA дочернего слова
    !                \ сохраняем адрес начала машинного кода в codeword дочернего кода
    EXIT             \ вкомпилить в DEFCONST вызов слова EXIT,
                     \ чтобы при исполнении DEFCONST осуществить возврат.
    (DOCON)          \ А дальше "немедленно" вкомпилить машинный код
;

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

8051DBC 3C 1D 05 08 08 44 45 46 43 4F 4E 53 54 00 00 00 <....DEFCONST...
        [LINK-----] len D  E  F  C  O  N  S  T __ __ __
8051DCC 88 8B 04 08 B0 96 04 08 3C 97 04 08 08 97 04 08 ........<.......
        [CODEWORD!] [WORD=====] [CREATE===] [[COMPILE]]
8051DDC 00 00 00 00 48 97 04 08 48 97 04 08 54 97 04 08 ....H...H...T...
                    [,-COMMA==] [,-COMMA==] [LBRAC====]
8051DEC 08 97 04 08 08 1E 05 08 4C 92 04 08 14 96 04 08 ........L.......
        [LIT======] [=8051E08=] [LATEST===] [@-FETCH==]
8051DFC D0 96 04 08 08 96 04 08 FC 95 04 08 8D 40 04 8B .............@..
        [>CFA=====] [!-STORE==] [EXIT=====] [===docon==
8051E0C 00 50 AD FF 20 FC 95 04 08 BC 1D 05 08 08 50 55 .P.. .........PU
        ==asmcode====] [EXIT=====] [LINK-----] len P  U
8051E1C 53 48 31 33 33 37 00 00 08 1E 05 08 37 13 00 00 SH1337......7...
         S  H  1  3  3  7 __ __ [=8051E08=]

Улучшая определяющие слова

Теперь, когда мы знаем, как создаются определяющие слова, настало время расширить их функционал и сделать более удобным их создание. Вместо компиляции машинного кода интерпретатора, мы будем использовать Forth-ассемблер. Попробуем переписать наш DEFCONST по-новому:

: DEFCONST
    WORD    \ прочтем слово с stdin
    CREATE  \ создадим заголовок слова
    0 ,     \ вместо codeword вкомпилим заглушку-ноль
    ,       \ скомпилируем param-field взяв его со стека (в нашем примере - 1337)

    ;CODE   \ завершить высокоуровневый код и начать низкоуровневый

    04 MOD-DISP-SHORT EAX EAX LEA        \   LEA   4(%EAX), %EAX
    MOD-DISP-NONE EAX EAX MOV-R32,R/M32  \   MOV   (%EAX), %EAX
    EAX PUSH                             \   PUSH  %EAX
    NEXT                                 \   NEXT

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

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

  • Все от : DEFCONST до ;CODE - высокоуровневый Forth-код, исполняемый при вызове слова родительского слова.
  • Все от ;CODE до END-CODE - это машинный код, исполняемый, когда дочернее слово исполняется. То есть, все начиная с ;CODE до END-CODE – это интерпретатор, на который будут указывать codeword-ы дочерних слов. ;CODE означает что высокоуровневая часть слова закончилась (";") и начинается определение в машинном коде. И нам больше не нужно создавать специальное слово для того чтобы вкомпилить интерпретатор в машинном коде.

От предыдущего варианта здесь есть два важных отличия:

  • определение теперь завершается не ; а словом END-CODE
  • слово ;CODE берет на себя всю работу, которую предыдущее определение делало вручную.

END-CODE отличается от ; только тем, что не добавляет в определение слово EXIT. Вот его реализация:

: END-CODE  ( -- )  \ Завершить ассемблерное определение
    LATEST @ HIDDEN EXIT
; IMMEDIATE

Как работает слово ;CODE?

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

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

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

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

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

: ;CODE
    ' (;CODE) ,      \ вкомпилить (;CODE) в определение
    [COMPILE] [      \ вкомпилить переход в immediate-режим
    \ ASSEMBLER         \ включить ассемблер (пока он всегда включен)
; IMMEDIATE          \ Это слово немедленного исполнения!

: (;CODE)
    R>                  \ pop-ит адрес машинного кода со стека возвратов
    LATEST @ >CFA       \ берет адрес codeword последнего слова
    !                   \ сохраняет адрес машинного кода в codeword создаваемого слова
;

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

Устройство DODOES

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

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

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

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

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

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

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

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

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

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

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

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

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

forth-interpret-44.png

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

  • (a) Push-нуть адрес param-field слова CHILD на стек данных для последующего использования в высокоуровневой подпрограмме. Инструкция CALL в момент своего выполнения не изменяет никаких регистров, поэтому мы ожидаем обнаружить адрес param-field слова CHILD, вычислив %EAX+4.
  • (b) Добыть адрес высокоуровневой подпрограммы, pop-нув его из стека CPU. Это адрес высокоуровневого кода.
  • (c) сохранить старое значение указателя интерпретации %esi(2) на стеке возвратов. C этого момента регистр %ESI будет использоваться при исполнении высокоуровневого фрагмента кода. По существу, DODOES должен использовать %ESI, подобно тому, как это делает DOCOL.
  • (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). Чтобы их понять, давайте посмотрим на пример их использования:

: MAKE-CONST ( n -- )
    WORD     \ прочтем слово с stdin
    CREATE   \ создадим заголовок слова
    0 ,      \ вместо codeword вкомпилим заглушку-ноль
    ,        \ скомпилируем param-field взяв его со стека (в нашем примере - 1337)
  DOES>      \ завершение "создающей" части, начало части "действия"
    @        \ прочесть значение из поля параметров слова,
             \разыменовать для получения содержимого
;

Сравните это с предыдущим примером ;CODE и заметьте, что DOES> выполняет функцию, аналогичную ;CODE. Во время исполнения MAKE-CONST будет работать все то, что определено между MAKE-CONST и DOES>. Это код, который формирует поле параметров дочернего слова. А во время исполнения дочернего слова будет работать все то, что определено от DOES> до ;, то есть высокоуровневый код. Так как с ;CODE оба класса: порождающий и действия содержатся внутри тела родительского Forth-слова, как показано на рисунке:

forth-interpret-45.png

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

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

  • (a) компилирует Forth-слово (DOES>) в MAKE-CONST.
  • (b) компилирует машинный код CALL DODOES в MAKE-CONST.

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

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

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

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

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

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

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

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

Приветствие

Это слово печатает версию и "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_docon>>

<<forth_inlining_asm>>

<<forth_welcome>>

<<forth_all>>

<<forth_docon>>

\ <<forth_defconst>>




<<forth_sub_semi_code>>

<<forth_semi_code>>

<<forth_endcode>>

\ <<forth_defconst_2>>

\ <<forth_push1337>>



<<forth_does>>

<<forth_make_const>>

LATEST @ @ @ 200 DUMP
Яндекс.Метрика
Home