1

1.3. Стек данных и вычисления

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

    имя     вершина стека до  --->  вершина стека после

    слова   исполнения слова        исполнения слова

При этом считаем, что самое верхнее значение в стеке (последнее добавленное) находится справа.

Для работы с собственно вершиной стека имеются следующие слова:

    DUP     A -> A, A

    DROP    A ->

    OVER    A, B --> A, B, A

    ROT     A, B, C --> B, C, A

    SWAP    A, B --> B, A

Слово DUP (от DUPLICATE - дублировать) дублирует вершину стека, добавляя в стек еще одно значение, равное тому, которое было до этого верхним. Слово DROP (сбросить) убирает верхнее значение. Слово OVER (через) дублирует значение, лежащее на стеке непосредственно под верхним. Слово ROT (от ROTATE - вращать) циклически переставляет по часовой стрелке три верхних значения в стеке. Наконец, слово SWAP (обменять) меняет местами два верхних значения.

Можно работать с любым элементом стека с помощью слов

    PICK  An, An-1, ... A0, n ---> An, An-1, ... A0, An

    ROLL  An, An-1, ... A0, n ---> An-1, ... A0, An

Слово PICK (взять) дублирует n-й элемент стека (считая от нуля), так что 0 PICK равносильно DUP, а 1 PICK равносильно OVER. Слово ROLL (повернуть) циклически переставляет n верхних элементов стека (тоже считая от нуля) по часовой стрелке, так что 2 ROLL равносильно ROT, 1 ROLL равносильно SWAP, а 0 ROLL является пустой операцией.

Чтобы "увидеть" верхнее значение на стеке, используется слово . (точка) A -->, которое снимает значение с вершины стека и печатает его на терминале как целое число в свободном формате (т.е. без ведущих нулей и со знаком минус, если число отрицательно). Вслед за последней цифрой числа слово-точка выводит один пробел, чтобы выводимые подряд числа не сливались в сплошной ряд цифр. Если программист хочет, чтобы напечатанное значение осталось на стеке, он должен исполнить текст DUP . . Слово DUP создаст копию верхнего значения, а точка ее распечатает и уберет со стека.

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

Теперь у нас достаточно средств, чтобы привести примеры диалога. Рассмотрим следующий протокол работы:

    >   5  6  7

    OK

    > SWAP . . .

    6 7 3 OK

В ответ на приглашение к вводу (знак > , печатаемый системой) программист вводит три числа: 5, 6 и 7. Обрабатывая введенный текст, форт-система кладет эти числа в указанном порядке на стек и по завершении обработки выводит подтверждающее сообщение OK и вновь приглашает программиста к вводу. Далее программист вводит текст из четырех слов: SWAP и три точки. Исполняя эти слова-команды, форт-система меняет местами два верхних элемента стека (5, 6, 7 -> 5, 7, 6) и затем поочередно три раза снимает верхнее значение со стека и печатает его. В результате на терминале появляется текст 6 7 5 и сообщение OK, указывающее на завершение обработки, после чего система вновь выдает программисту приглашение на ввод.

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

    DECIMAL    --->   десятичная

    HEX        --->   шестнадцатеричная

    OCTAL      --->   восьмиричная

Первоначально устанавливается десятичная система. Если в процессе работы будет исполнено, например, слово HEX (от HEXADECIMAL - шестнадцатиричная), то при дальнейшем вводе и выводе чисел будет использоваться шестнадцатиричная система с цифрами от 0 до 9 и от A до F до тех пор, пока основание системы счисления не будет вновь изменено. Внутренним же представлением чисел является обычный двоичный дополнительный код, применяемый в большинстве существующих ЭВМ.

Слова-команды, выполняющие арифметические операции над числами, являются общепринятыми математическими обозначениями:

    +       A,B  --->  сумма A+B

    -       A,B  --->  разность A-B

    *       A,B  --->  произведение A*B

    /       A,B  --->  частное от A/B

    MOD     A,B  --->  остаток от A/B

    /MOD    A,B  --->  остаток от A/B, частное от A/B

    ABS       A  --->  абсолютная величина A

    NEGATE    A  --->  значение с обратным знаком -A

    1+        A  --->  A+1

    1-        A  --->  A-1

    2+        A  --->  A+2

    2-        A  --->  A-2

    2/        A  --->  частное от A/2

При сложении, вычитании и умножении не учитывается возможность переполнения, в случае его возникновения используются младшие 16 разрядов результата. Такая арифметика называется арифметикой по модулю 65536 (2 в степени 16); ее основное достоинство состоит в том, что она дает одинаковые в двоичном представлении результаты независимо от того, как понимаются операнды: как числа со знаком в диапазоне от -32768 до +32767 или как числа без знака в диапазоне от 0 до 65535.

Операции деления /, MOD и /MOD рассматривают свои операнды как числа со знаком. Из нескольких известных математических определений деления с остатком (которые по-разному трактуют случаи, когда операнды имеют разные знаки или оба отрицательны) язык Форт использует так называемое деление с нижней границей: остаток имеет знак делителя или равен нулю, а частное округляется до его арифметической нижней границы ("пола") [11]. Во многих ЭВМ, имеющих аппаратную реализацию деления, применяются другие правила для определения частного и остатка, однако это обычно не вызывает трудностей при программировании, поскольку самый важный случай с неотрицательными операндами все определения трактуют одинаково.

При выполнении деления возможно возникновение ошибочной ситуации, если делитель - нуль или частное не умещается в 16 разрядов (переполнение, возникающее при делении -32768 на -1).

Одноместные операции ABS и NEGATE игнорируют переполнение, возникающее в том единственном случае, когда операндом является число -32768, возвращая в качестве результата 0.

Наконец, одноместные операции 1+, 1-, 2+, 2- выполняют действие, отраженное в их мнемонике: увеличение или уменьшение значения на вершине стека на 1 или 2; аналогично слово 2/ возвращает частное от деления своего параметра на 2. Эти слова включены в стандарт ввиду частного использования соответствующих действий.

Использование стека для хранения промежуточных значений естественным образом приводит к так называемой "обратной польской форме" - одному из способов бесскобочной записи арифметических выражений, подразумевающему постановку знака операции после операндов. Например, выражение (A/B+C)*(D*E-F*(G-H)) записывается следующим образом: A B / C + D E * F G H - * - * . Очевидно, что этот текст выполним для Форта, если A, B и т.д.- слова, которые кладут на стек по одному числу. Таким образом, форт-систему можно использовать как калькулятор. Чтобы вычислить, например, значение (25+18+32)*5, достаточно ввести такой текст: 25 18 + 32 + 5 * . . В ответ система напечатает (исполняя точку) ответ -375.

Чтобы повысить точность вычислений в последовательности умножение-деление, стандарт предусматривает два необычных слова:

    */     A,B,C ---> частное от (A*B/C)

    */MOD  A,B,C ---> остаток, частное от (A*B/C)

Особенность этих слов состоит в том, что промежуточный результат A*B вычисляется с двойной точностью, и в качестве делимого для C используются все его 32 разряда. Окончательные же результаты являются 16-разрядными числами.

Наряду с описанной выше 16-разрядной арифметикой, язык Форт имеет полный набор средств для работы с 32-разрядными целыми числами через стандартное расширение двойной точности. Внутренним представлением таких чисел является 32-разрядный двоичный дополнительный код, представляющий их как числа со знаком в диапазоне от -2147483648 до +2147483647 или как числа без знака в диапазоне от 0 до 4294967295. При размещении в стеке числа двойной точности занимает два элемента: верхний - старшая половина, предыдущий - младшая. Такое расположение делает простым переход от двойной точности к обычной через слово DROP. Расширение двойных чисел включает следующие слова:

    2DROP    AA --->

    2DUP     AA ---> AA,AA

    2OVER    AA,BB ---> AA,BB,AA

    2ROT     AA,BB,CC --> BB,CC,AA

    2SWAP    AA,BB ---> BB,AA

    D.       AA --->

    D+       AA,BB ---> сумма AA+BB

    D-       AA,BB ---> разность AA-BB

    DABS     AA ---> абсолютная величина AA

    DNEGATE  AA ---> число с обратный знаком -AA

Здесь обозначение типа AA подчеркивает, что значение занимает два элемента стека. Хотя стандартное расширение этого не предусматривает, во многих реализациях языка Форт этот ряд продолжают операции умножения и деления:

    D*      AA,BB ---> произведенме AA*BB

    D/      AA,BB ---> частное от AA/BB

    DMOD    AA,BB ---> остаток от AA/BB

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

Следующие два слова являются переходными между арифметическими операциями одинарной и двойной точности; их мнемоника включает букву M слова MIX - смесь) и U (от слова UNSIGNED - беззнаковый):

    UM*      A,B ---> CC

    UM/MOD   AA,B ---> C,D

Слово UM* перемножает операнды A и B как 16-разрядные числа без знака, возвращая все 32 разряда получившегося произведения. Слово UM/MOD рассматривает 32-разрядное делимое AA и 16-разрядный делитель B как числа без знака и возвращает получающиеся 16-разрядные остаток C и частное D. Если делитель нуль или частное превышает 65535, то это рассматривается как ошибка. Для перехода к двойной точности с учетом знака многие реализации имеют слово S>D A --> AA, которое расширяет исходное число A до числа двойной точности распространением знакового разряда.

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

    > 1234567.  7654321.  D+  D.

    8888888 ОК

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


1

1.5. Константы и переменные, работа с памятью

Программисту часто бывает удобно работать не с "анонимными" значениями, а с именованными. По аналогии со средствами других языков эти средства языка Форт называются константами и переменными. Впоследствии мы увидим, что они являются не "изначальными", а (наряду с определениями через двоеточие) частными случаями более общего понятия "определяющие слова".

Слово CONSTANT (константа) А --> работает следующим образом. Со стека снимается верхнее значение, а из входного текста выбирается очередное слово и запоминается в словаре как новая команда. Ее действие состоит в следующем: поместить на стек значение А, снятое со стека в момент ее определения. Например, 4 CONSTANT XOP . В дальнейшем при исполнении слова XOP число 4 будет положено на стек.

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

Работа с переменной помимо получения ее адреса состоит в получении ее текущего значения и присваивании нового. Для этого язык Форт имеет следующие слова:

    @    A   ---> B

    !    B,A --->

Слово @ (читается "разыменовать") снимает со стека значение и, рассматривая его как адрес области оперативной памяти, кладет на стек двухбайтное значение, находящееся по этому адресу. Обратное действие выполняет слово ! (восклицательный знак, читается "присвоить"), которое снимает со стека два значения и, рассматривая верхнее как адрес области оперативной памяти, засылает по нему второе снятое значение. Эти слова можно использовать не только для переменных, но и для любых адресов оперативной памяти. Следующий протокол работы показывает порядок использования переменной в сочетании с этими словами:

    > VARIABLE  X  1  X  !

    OK

    > X  @  .

    1  ОК

    > Х @ NEGATE  X  !  X  .

    -1  ОК

В первой строке определяется переменная X, и ей присваивается начальное значение 1. Затем текущее значение переменной X распечатывается. После этого текущее значение меняется на противоположное по знаку и вновь распечатывается.

Полезным вариантом слова ! является слово +! (плюс-присвоить) N,A --> , которое увеличивает на N значение, находящееся в памяти по адресу A. Несмотря на то, что это слово легко выразить через + и !:

    : +!  ( N,A ---> )  DUP @ ROT + SWAP ! ;

оно включено в обязательный набор слов.

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

Для работы со значениями двойной длины имеются слова

    2CONSTANT    AA --->

    2VARIABLE       --->

    2@            A ---> BB

    2!         BB,A --->

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

Константы и переменные позволяют программисту использовать память в словаре вполне определенным образом. А как быть, если требуется что-то иное? Общий принцип языка Форт состоит в том, чтобы не закрывать от программиста даже самые элементарные единицы, из которых строятся его более сложные слова, а предоставлять их наравне с другими словами. Элементарными словами для работы с памятью в словаре, помимо приведенных выше @ и ! , являются следующие:

    HERE      ---> A

    ALLOT   A --->

    ,       A --->

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

Слово ALLOT (распределить) резервирует в словаре область памяти, размер которой (в байтах) снимает со стека. Резервирование состоит в том, что текущее значение указателя изменяется на заданную величину, поэтому при положительном значении запроса память отводится вплотную от вершины словаря, а при отрицательном значении запроса происходит освобождение соответствующего участка памяти. Наконец, слово , (запятая) выполняет так называемую "компиляцию" значения, снимаемого со стека: значение переносится на вершину словаря, после чего указатель вершины продвигается на 2 (размер в байтах скомпилированного значения). Некоторые из приведенных слов легко выражаются через другие:

    : , ( A --> )  HERE ! 2 ALLOT ;

    : 2@ ( A --> BB )  DUP 2 + @ SWAP @ ;

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

А как создать в словаре поименованную область памяти? Можно завести область и связать ее адрес с именем через описание константы: HERE 10 ALLOT CONSTANT X10. Слово HERE оставляет на стеке адрес текущей вершины словаря, затем при исполнении текста 10 ALLOT от этой вершины резервируется 10 байт, после чего слово CONSTANT связывает адрес зарезервированной области с именем X10. В дальнейшем при исполнении слова X10 этот адрес будет положен на стек.

Другой возможный путь состоит в использовании слова CREATE (создать) --> в таком контексте: CREATE X10 10 ALLOT . Слово CREATE, подобно слову VARIABLE, выбирает из входной строки очередное слово и определяет его как новую команду с таким действием: положить на стек адрес вершины словаря на момент создания этого слова. Поскольку следующие действия в приведенном примере резервируют память, то слово X10 будет класть на стек адрес зарезервированной области. Очевидно, что слово VARIABLE можно выразить через CREATE

    : VARIABLE  ( ---> )  CREATE 2 ALLOT ;

или иначе (если мы хотим инициализировать нулем значение создаваемой переменной):

    : VARIABLE  ( ---> )  CREATE 0 , ;

(Другой аспект использования слова CREATE рассматривается в п. 1.10).

В стандарте определен ряд системных переменных, к которым программист может свободно обращаться, в том числе STATE (состояние) -> А и BASE (основание) --> A. Исполнение каждого из этих слов заключается в том, что на стеке оставляется адрес ячейки, в которой хранится значение данной переменной.

Переменная STATE представляет текущее состояние текстового интерпретатора: нуль для исполнения и не нуль (обычно -1) для компиляции. Поэтому вся реализация слов [ и ] , переключающих интерпретатор из одного состояния в другое, сводится к одному присваиванию:

    : [ ( ---> )  0 STATE ! ; IMMEDIATE

    : ] ( ---> )  -1 STATE ! ;

Переменная BASE хранит текущее основание системы счисления для ввода - вывода чисел, поэтому реализация слов для установки стандартных систем выглядит так:

    : DECIMAL  ( ---> )  10 BASE ! ;

    : HEX      ( ---> )  16 BASE ! ;

Отсюда следует более изящный способ кратковременной смены системы счисления во время компиляции определения: [ BASE @ HEX [ FF00 [ BASE ! ] . Сначала на стеке запоминается текущее основание, и система счисления переключается на основание 16, в котором и воспринимается следующее число FF00, после чего восстанавливается прежнее основание. А как узнать текущее основание системы счисления? Исполнение текста BASE @ не поможет, поскольку ответом всегда будет 10 (почему?). Правильный ответ даст исполнение текста BASE @ DECIMAL . , в результате чего значение основания будет напечатано как число в десятичной системе. Еще более правильным было бы использовать текст BASE @ DUP DECIMAL . BASE ! , который после печати основания в десятичной системе восстанавливает его прежнее значение.


1

1.6. Логические операции

В языке Форт имеется только один тип значений - 16-разрядные двоичные числа, которые, как мы видели, рассматриваются в зависимости от ситуации как целые числа со знаком или как адреса и т. д. Точно так же подходят и к проблеме представления логических значений ИСТИНА и ЛОЖЬ: число 0, в двоичном представлении которого все разряды нули, представляет значение ЛОЖЬ, а любое другое 16-разрядное значение понимается как ИСТИНА. Вместе с тем стандартные слова, которые должны возвращать в качетве результата логическое значение, из всех возможных представлений значения ИСТИНА используют только одно: число -1 (или, что то же самое, 65535), в двоичном представлении которого все разряды единицы. Такое соглашение связано с тем, что традиционные логические операции конъюнкции, дизъюнкции и отрицания выполняются в Форте поразрядно над всеми шестнадцатью разрядами операндов:

    AND   A,B ---> A  B   логическое И

    OR    A,B ---> A  B   логическое ИЛИ

    XOR   A,B ---> A  B   исключающее ИЛИ

    NOT     A --->  ^ A   логическое НЕ

Как и в предыдущих случаях, эти операции не являются независимыми: операция отрицания (поразрядное инвертирование) легко выражается через исключающее ИЛИ (поразрядное сложение по модулю два):

    : NOT ( A --> ^A )  -1 XOR ;

Нетрудно увидеть, что для принятого в Форте стандартного представления значений ИСТИНА и ЛОЖЬ все эти слова работают, как обычные логические операции.

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

    <  A,B ---> A < B   меньше

    =  A,B ---> A > B   равно

    >  A,B ---> A > B   больше

Эти операции снимают со стека два врехних значения, сравнивают их как числа со знаком (операция "равно" выполняет поразрядное сравнение) и возвращают результат сравнения как значение ИСТИНА и ЛОЖЬ в описанном выше стандартном представлении. Из-за стремления к минимизации обязательного набора операций в него не включены слова для операций смешанного сравнения, поскольку их легко выразить через уже имеющиеся:

    : <=  ( A,B ---> A <= B )  SWAP < NOT ;

    : >=  ( A,B ---> A >= B >  SWAP > NOT ;

    : <>  ( A,B ---> A <> B )  = NOT ;

Для сравнения 16-разрядных чисел без знака имеется слово U< A,B --> A < B. Эта операция обычно используется для сравнения адресов, которые лежат в диапазоне от 0 до 65535. Буква и (от UNSIGNED - беззнаковый) в ее мнемонике говорит о том, что операнды рассматриваются как числа без знака.

Ввиду частого использования и возможности непосредственной реализации на многих существующих ЭВМ в обязательный набор слов включены одноместные операции сравнения с нулем:

    0<   A ---> A < 0

    0=   A ---> A = 0

    0>   A ---> A > 0

При этом слово 0= можно использовать вместо NOT как операцию логического отрицания, и в отличие от NOT оно будет правильно работать при любых представлениях логического значения ИСТИНА.

Описанные выше двухместные операции сравнения естественным образом выражаются через сравнения с нулем:

    : <  ( A,B ---> A < B )  - 0< ;

    : =  ( A,B ---> A = B )  - 0= ;

    : >  ( A,B ---> A > B )  - 0> ;

Стандартное расширение двойных чисел имеет аналогичные слова для сравнения 32-разрядных значений:

    D0=  AA ---> AA = 0

    D<   AA,BB ---> AA < BB

    D=   AA,BB ---> AA = BB

    DU<  AA,BB ---> AA < BB

Слова D< и DU< различаются тем, что первое рассматривает свои операнды как числа со знаком, а второе - как числа без знака. Для слов D0= и D= такое различие несущественно, их, например, можно определить так:

    : D0=  ( AA ---> AA = 0 )   OR 0= ;

    : D=   ( AA,BB ---> AA = BB )  D- D0= ;

Слово OR (логическое ИЛИ) в определении слова D0= логически складывает старшую и младшие половины исходного 32-разрядного значения. Нулевой результат будет получен тогда и только тогда, когда исходное значение было нулевым. Следующее слово 0= преобразует этот результат к логическому значению в стандартном представлении. Исполнение слова D= состоит в вычислении разности его операндов в сравнении этой разности с нулем.


1

1.7. Структуры управления

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

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

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

Условный оператор строится с помощью слов:

    IF     А --->    (исполнение)

    ELSE     --->    (исполнение)

    THEN     --->    (исполнение)

Внутри определения через двоеточие отрезок текста IF <часть-то> ELSE <часть-иначе> THEN задает следующую последовательность действий. Слово IF (если) снимает значение с вершины стека и рассматривает его как логическое. Если это ИСТИНА (любое нулевое значение), то выполняется часть "то" - слова, находящиеся между IF и THEN, а если ЛОЖЬ (равно нулю), то исполняется часть "иначе" - слова между ELSE и THEN . Сами слова ELSE (иначе) и THEN (то) играют роль ограничителей для слова IF и самостоятельной семантики не имеют. Часть <иначе> вместе со словом ELSE может отсутствовать, и тогда условный оператор имеет сокращенную форму IF <часть-то> THEN . Если логическое значение, снимаемое со стека словом IF, ИСТИНА, то выполняются слова, составляющие часть "то", а если ЛОЖЬ, то данный оператор не выполняет никаких действий. Обратите внимание, что условие для слова IF вычисляется предшествующими словами.

Для примера рассмотрим определение слова ABS, вычисляющего абсолютное значение числа:

    : ABS ( A --->абс A) DUP 0< IF NEGATE THEN ;

Слово DUP дублирует исходное значение, следующее слово 0< проверяет его знак, заменяя копию на логическое значение - результат проверки. Слово IF снимает со стека этот результат, и если это ИСТИНА, то лежащее на стеке исходное отрицательное значение словом NEGATE заменяется на противоположное.

Еще пример: стандартное слово ?DUP дублирует верхнее значение, если это не ноль, и оставляет стек в исходном состоянии, если на вершине ноль:

    : ?DUP ( A -> A,A/0 )  DUP  IF DUP THEN ;

В спецификации данного слова косая черта разделяет два варианта результата, который это слово может оставить на стеке. Примером использования полного условного оператора может служить определение слова S>D, расширяющего исходное значение до значения двойной длины распространением знакового разряда:

    : S>D   ( A -> AA )  DUP 0< IF -1 ELSE 0 ТНЕN ;

Это слово добавляет в стек -1, если число на вершине стека отрицательно, или 0 в противном случае. Таким образом, выполняется распространение знакового раз- ряда на старшую половину значения двойной точности.

В стандарт языка Форт включены циклы с условием и циклы со счетчиком. Циклы первой группы образуются с помощью слов

    BEGIN     --->  (исполнение)

    UNTIL   A --->  (исполнение)

    WHILE   A --->  (исполнение)

    REPEAT    --->  (исполнение)

и имеют две формы:

    BEGIN <тело> UNTIL

    BEGIN <тело-1> WHILE <тело-2>  REPEAT

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

Цикл BEGIN-UNTIL называется циклом с проверкой в конце. После исполнения слов, составляющих его тело, на стеке остается логическое значение - условие завершения цикла. Слово UNTIL (пока не) снимает это значение со стека и анализирует его. Если это ИСТИНА (не нуль), то исполнение цикла завершается, т. е. далее будут исполняться слова, следующие за UNTIL, а если это ЛОЖЬ (нуль), то управление возвращается к началу цикла от слова BEGIN. Например, определение слова, вычисляющего факториал, может выглядеть так:

          : ФАКТОРИАЛ ( N ---> N!      ВЫЧИСЛЕНИЕ N ФАКТОРИАЛ)

              DUP 2 < IF DROP 1      ( 1    ЕСЛИ N<2, ТО N!=1)

                     ELSE            ( N    ИНАЧЕ            )

                 DUP                 ( S,K     S=N,K=N       )

               BEGIN                 ( S,K                   )

                    1-               ( S,K'    K'=K-1        )

                     SWAP  OWER      ( K',S,K'               )

                     *  SWAP         ( S',K'   S'=S*К'       )

                    DUP  1  =        ( S',K',К'=1            )

               UNTIL                 ( S',1    S'=N!         )

            DROP  THEN  ;            ( N!                    )

Как и ранее, в комментариях, сопровождающих каждую строчку текста, мы указываем значения, которые остаются на вершине стека после ее исполнения. Такое документирование облегчает понимание программы и помогает при ее отладке.

Цикл с проверкой в начале BEGIN-WHILE-REPEAT используется, когда в цикле есть действия, которые не надо выполнять в заключительной итерации. Исполнение слов, составляющих его тело-1, оставляет на стеке логическое значение - условие продолжения цикла. Слово WHILE (пока) снимает это значение со стека и анализирует его. Если это ИСТИНА (не нуль), то исполняются слова, составляющие тело-2 данного цикла до ограничивающего слова REPEAT (повторять), после чего вновь исполняется тело-1 от слова BEGIN. Если же значение условия ЛОЖЬ (нуль), то исполнение данного цикла завершается и начинают выполняться слова, следующие за REPEAT. Заметьте, что в отличие от цикла BEGIN-UNTIL, значение ИСТИНА соответствует продолжению цикла. Для примера рассмотрим программу вычисления наибольшего общего делителя по алгоритму Евклида:

          : НОД   (  A,B ---> C:НАИБОЛЬШИЙ ОБЩИЙ ДЕЛИТЕЛЬ)

            2DUP  <  IF  SWAP  THEN   ( ТЕПЕРЬ  A>=B     )

               BEGIN  DUP             ( A,B,B            )

               WHILE                  ( ПОКА B НЕ НОЛЬ   )

                      2DUP  MOD       ( A,B,C:ОСТАТОК    )

                    ROT               ( B,C,A            )

                    DROP              ( A',B'  A'=B,B'=C )

               REPEAT DROP ;          ( НОД              )

Для организации циклов с целочисленной переменной - счетчиком цикла - используются слова

    DO     A,B --->     (исполнение)

    LOOP       --->     (исполнение)

    +LOOP    A --->     (исполнение)

    I          ---> A   (исполнение)

    J          ---> A   (исполнение)

    LEAVE      --->     (исполнение)

Такие циклы записываются в одной из следующих двух форм: DO <тело> LOOP или DO <тело> +LOOP. В обоих случаях цикл начинается словом DO (делать), которое снимает со стека два значения: начальное (на вершине стека) и конечное (второе сверху) - и запоминает их. Текущее значение счетчика полагается равным начальному значению, после чего исполняются слова, составляющие тело цикла. Слово LOOP увеличивает текущее значение счетчика на единицу и проверяет условие завершения цикла. В отличие от него, слово +LOOP прибавляет к текущему значению счетчика значение шага, которое вычисляется на стеке телом цикла и рассматривается как число со знаком. В обоих случаях условием завершения цикла является пересечение границы между A-1 и A при переходе от прежнего значения счетчика к новому (направление перехода определяется знаком шага), где A-конечное значение, снятое со стека словом DO.

Если пересечения не произошло, то тело цикла исполняется вновь с новым значением счетчика в качестве текущего.

Такое определение позволяет рассматривать исходные параметры цикла (начальное и конечное значения) и как числа со знаком, и как числа без знака (адреса). Например, текст 10 0 DO (тело) LOOP предписывает выполнять тело цикла 10 раз со значениями счетчика 0, 1, 2, ..., 9, а в случае 0 10 DO (тело) LOOP тело цикла будет исполнено 65 526 раз со значением счетчика 10, 11, ..., 32767, -32768, -32767, .... -1 или (что то же самое) со значениями счетчика 10, 11, ..., 65535.

В то же время цикл 0 10 DO <тело> -1 +LOOP будет исполняться 11 раз (а не 10) со значениями счетчика 10, 9, ..., 0, поскольку пересечение границы между -1 и 0 произойдет при переходе от значения счетчика 0 к следующему значению - 1. Цикл 10 0 DO <тело> -1 +LOOP будет исполняться 65 527 раз со значениями счетчика 0, -1, -2, ..., -32768, 32767, ..., 10 или (что то же самое) 0, 65535, 65534, ..., 10. Таким образом, цикл со счетчиком всегда выполняется хотя бы один раз. Некоторые реализации предусматривают слово ?DO, которое не исполняет тело цикла ни разу, если начальное и граничное значения оказались одинаковыми.

Внутри тела цикла слово I кладет на стек текущее значение счетчика. Например, следующее определение вычисляет сумму квадратов первых n натуральных чисел:

          : SS2   ( N ---> S:СУММА КВАДРАТОВ ОТ 1 ДО N)

              0  SWAP           ( 0,N         S[0]=0  )

              1+  1             ( S[0],N+1,1          )

                    DO   I      ( S[I-1],I            )

                      DUP       ( S[I] S[I]=S[I-1]+I*I)

                    LOOP  ;     ( S[N]   )

Слово LEAVE (уйти), употребленное внутри цикла, вызывает прекращение исполнения его тела; управление переходит к словам следующим за словом LOOP или +LOOP.

В программировании часто применяется конструкция цикл в цикле. Чтобы во внутреннем цикле получить текущее значение счетчика объемлющего цикла, используется слово J: DO ... I ... DO ... I ... J ... LOOP ... I ... LOOP. Первое вхождение слова I дает текущее значение счетчика внешнего цикла. Следующее вхождение I дает. уже значение счетчика внутреннего цикла. Чтобы получить счетчик внешнего цикла, надо использовать слово J.

По выходе из внутреннего цикла доступ к этому значению вновь дает слово I. Этим слова I и J отличаются от переменных цикла в традиционных языках программирования. Некоторые реализации предусматривают еще и слово К для доступа к счетчику третьего объемлющего цикла.


1

1.8. Литеры и строки, форматный вывод чисел

В современном программировании важное место занимает обработка текстовых данных. С каждой литерой, которую можно ввести с внешнего устройства или вывести на него, связывается некоторое число - код этой литеры, так что в памяти ЭВМ литеры представлены своими кодами. Стандарт языка Форт предусматривает использование таблицы кодов ASCII, в которой задействованы все числа в диапазоне от 0 до 127. Каждый код занимает один 8-разрядный байт, в котором для представления литеры используются младшие 7 разрядов.

Такая привязка к одной конкретной кодировке не является существенным препятствием к использованию других, если сохраняется условие, что код литеры занимает один байт. В применяемых в нашей стране форт-системах помимо ASCII применяются коды КОИ-7, КОИ-8, ДКОИ и другие.

Для доступа к однобайтным значениям, расположенным в памяти, используются слова C@ A-->B и C! B,A -->, которые аналогичны словам @ и !. Префикс С (от слова CHARACTER - литера) говорит о том, что эти слова работают с литерными кодами (однобайтными значениями).

Слово C@ возвращает содержимое байта по адресу A, дополняя его до 16-разрядного значения нулевыми разрядами. Обратное действие выполняет слово C!, которое засылает младшие 8 разрядов значения B в память по адресу A.

Для политерного обмена с терминалом стандарт предусматривает такие слова:

    KEY         ---> A

    EMIT      A --->

    CR          --->

    TYPE    A,N --->

    EXPECT  A,N --->

Слово KEY (клавиша) возвращает в младших разрядах значения A - код очередной литеры, введенной с терминала. В отличие от слова C@ старшие разряды зависят от реализации и могут быть ненулевыми. Обратное действие выполняет слово EMIT (испустить), которое снимает значение со стека и, рассматривая его младшие разряды как код литеры, выводит эту литеру на терминал. Специальное слово CR (сокращение от CARRIAGE RETURN - возврат каретки) выполняет перевод строки при выводе на терминал.

Расположенные в памяти побайтно коды литер образуют строку, которую можно напечатать на терминале словом TYPE (напечатать). Это слово снимает со стека число - количество литер (оно должно быть неотрицательно) и адрес начала строки (ее первого байта):

          : TYPE ( A,N ---> )    ?DUP IF 0 DO

                DUP I + C@ EMIT LOOP THEN DROP ;

Если число литер равно нулю, то ничего не печатается.

Обратное действие - ввод строки литер - выполняет слово ЕХРЕСТ (ожидать), которое снимает со стека длину и адрес области памяти для размещения вводимых литер. Коды литер, последовательно вводимых с терминала, помещаются в указанную область до тех пор, пока не будет введено заданное число литер или не будет введена управляющая литера "возврат каретки" (код этой литеры в память не заносится). Фактическое число введенных литер сообщается в стандартной переменной SPAN (размер), эти литеры к тому же отображаются на терминале.

Ввиду его особой важности для кодирования пробела выделена специальная константа BL (от BLANK-пробел), которую для кода ASCII можно задать так: 32 CONSTANT BL. При исполнении слова BL на стеке остается код пробела. Чтобы вывести пробел на терминал, имеются следующие стандартные слова:

          : SPACE ( ---> )   BL EMIT ;

          : SPACES ( К ---> )  ?DUP IF 0 DO SPACE LOOP THEN ;

Слово SPACE (пробел) выводит на терминал один пробел, а слово SPACES (пробелы) - несколько, снимая их количество со стека (это значение, как и длина строки в слове TYPE, должно быть неотрицательным).

Внутри определений через двоеточие можно использовать явно заданные тексты для вывода их на терминал. При исдолнении слова ." (точка и кавычка), употребленного в контексте ." текст ", следующие за ним литеры до закрывающей кавычки исключительно печатаются на терминале. Пробел, отделяющий слово .", в число печатаемых литер не входит. Другое слово .( (точка и скобка) отличается от ." тем, что ограничителем текста является закрывающая правая скобка, и это слово можно использовать как внутри определений, так и вне их.

Помимо строки - поля байт, длина которого задается отдельно - язык Форт использует строки со счетчиком. Строка со счетчиком представляется полем байт, причем в первом байте записана длина строки. Стандарт не определяет форму представления счетчика длины, оставляя решение этого вопроса на усмотрение разработчиков конкретной реализации. Для перехода от строки со счетчиком к строке с явно заданной длиной имеется слово COUNT (счетчик) A --> B,N, которое преобразует адрес A строки со счетчиком в адрес B ее первой литеры (обычно это A+1) и значение счетчика. Строки со счетчиком используются при вводе слов из входной строки. Стандартное слово WORD (слово) C--> A снимает со стека код литеры-ограничителя и выделяет из входной строки подстроку, ограниченную этой литерой (начальные вхождения литеры-ограничителя пропускаются). Из выделенной подстроки формируется строка со счетчиком, адрес которой возвращается в качестве результата. Слова-команды языка Форт вводятся исполнением текста BL WORD, а текстовая строка в слове ." - исполнением текста QUOTE WORD, где слово QUOTE - константа, обозначающая код кавычки. Литеры введенной строки обычно располагаются вслед за вершиной словаря, т. е. в незащищенном месте, и поэтому их нужно как-то защитить, если предполагается их дальнейшее использование. Некоторые реализации предусматривают слово " (кавычка), которое используется внутри определения через двоеточие и во время его исполнения кладет на стек адрес следующей строки как строки со счетчиком. Это позволяет работать с явно заданными текстами.

Чтобы программист мог задавать коды литер, не связывая себя конкретной кодировкой, во многие реализации введено слово С" , которое кладет на стек код первой литеры следующего слова и может использоваться как внутри определения через двоеточие, так и вне его:

          : C" ( ---> C )   BL WORD COUNT DROP

                  C@ [COMPILE] LITERAL ; IMMEDIATE

Исполнение текста BL WORD COUNT DROP C@ оставляет на стеке код первой литеры следующего слова. Далее этот код нужно либо скомпилировать как число, либо оставить на стеке в зависимости от текущего состояния текстового интерпретатора. Для этого используется уже известное нам слово LITERAL. Однако включить его непосредственно в текст нельзя, так как это слово имеет признак немедленного исполнения и будет исполняться во время компиляции данного определения. Чтобы скомпилировать слово с признаком немедленного исполнения, используется слово [COMPILE] (от COMPILE - компилировать) --> (компиляция), которое само имеет такой признак. Оно принудительным образом компилирует следующее за ним слово независимо от наличия у него признака немедленного исполнения. Таким образом, ввод строки, ограниченной кавычкой, с помощью слова C" можно задать так: C" " WORD. Такой текст более нагляден, чем тот, в котором используется конкретный код или обозначающая его константа.

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

          : PAD ( ---> А )  HERE 100 + ;

В данном случае предполагается, что размер буфера не будет превышать 100 байт.

Собственно форматное преобразование начинается словом <# которое устанавливает служебную переменную HLD на конец буфера PAD:

          : <# ( ---> ) PAD HLD ! ;

Занесение очередной литеры в буфер PAD выполняет слово HLD (сохранить):

          : HLD ( С ---> )  -1 HLD +! HLD @ C! ;

Преобразование числа выполняет слово # DD1 --> DD2, которое работает со значениями двойной длины. Параметр делится на текущее значение переменной BASE (основание системы счисления) и заменяется на стеке получившимся частным, а остаток переводится в литеру соответствующую ему как цифра в данной системе счисления, и через слово HOLD эта литера добавляется в буфер PAD. Полный перевод числа выполняет слово #S:

          : #S ( DD ---> 0,0 )  BEGIN # 2DUP D0= UNTIL ;

которое выделяет цифры числа последовательным делением, пока не получится нуль. Наконец, слово #> завершает форматное преобразование, возвращая адрес и длину получившейся текстовой строки:

          : #> ( DD ---> A,N )  2DROP HLD @ PAD OVER - ;

Для вывода знака "минус" имеется слово SIGN (знак):

          : SIGN ( A ---> )  0< IF C" - HOLD THEN ;

которое добавляет в буфер PAD знак "минус", если параметр на вершине стека (число одинарной точности) отрицателен.

С помощью перечисленных средств легко определить стандартные слова D. и . для печати чисел и в свободном (минимальном) формате:

          : D.  ( DD ---> )

             2DUP DABS      ( DD,DDABS      )

              <#  #S        ( DD,0,0        )

                  ROT       ( D-МЛ,0,0,D-СТ )

                  SIGN      ( D-МЛ,0,0      )

               #>           ( D-ML,A,N      )

             TYPE SPACE DROP ;

          : .  ( N ---> ) S>D D. ;

Слово D. сначала переводит абсолютное значение исходного числа в строку литер, потом добавляет к ней возможный знак "минус", анализируя для этого старшую половину первоначального значения, и затем печатает получившуюся строку, выводя после нее еще один пробел. Слово . дополняет свой параметр до значения двойной длины распространением знакового разряда и обращается к слову D. для печати получившегося числа. Аналогичным образом реализуются стандартные слова D.R и .R , которые печатают число в поле заданного размера вплотную к его правому краю (отсюда в их мнемонике присутствует буква R от RIGHT-правый), добавляя арн необходимости начальные пробелы:

          : D.R  ( DD:ЧИСЛО,F:РАЗМЕР ПОЛЯ ---> )

              OVER  2SWAP  DABS       ( D-СТ,F,DD,DABS )

               <#  #S  ROT  SIGN  #>  (  F,A,N )

              ROT  OVER  -            ( A,N,F-A )

              DUP  0>  IF SPACES ELSE DROP THEN  TYPE ;

          : .R (N:ЧИСЛО,F:РАЗМЕР ПОЛЯ ---> )

              SWAP S>D D.R ;

В заключение рассмотрим программу шестнадцатиричиой распечатки областей памяти словом DUMP (дамп), которое получает на стеке адрес области и ее длину:

          : DUMP ( A:АДРЕС,N:ДЛИНА ---> )

              OVER  BASE  @  HEX  2SWAP         ( A,B,A,N )

              +  ROT  -2  AND                   ( B,A+N,A )

              DO  I  <#  C" *  HOLD             ( B,AI )

                0 15 DO  DUP  I  +              ( B,AI,AI+J )

                 C@  DECODE  HOLD  -1 +LOOP     ( B,AI )

                         C" *  HOLD             ( B,AI )

                0 14 DO  BL HOLD  DUP           ( B,AI,AI+J )

                 @ 0  # #  2DROP  -2 +LOOP      ( B,AI )

            BL HOLD  BL HOLD  0  # # # # #>     ( B,AT,NT )

                   CR TYPE  16 +LOOP   BASE ! ;

Внешний цикл с шагом 16 формирует и печатает текстовую строку. Первый внутренний цикл с шагом -1 засылает в буфер PAD литерные значения, соответствующие распечатываемым байтам. Здесь слово DECODE C-->C/C1 заменяет код C на некоторый код C1 (например, код литеры "точка"), если он не является кодом литеры, которую можно напечатать на данном терминале. Второй внутренний цикл с шагом -2 засылает в буфер четыре шестнадцатиричные цифры - представления двухбайтных значений, разделяя их одним пробелом. Далее в буфере PAD (т. е. в начале строки) формируется адрес тоже в виде четырехзначного числа. Перед началом работы устанавливается шестнадцатиричная система счисления, а в конце восстанавливается первоначальная. Следующий протокол работы показывает результат исполнения слова DUMP в конкретном случае:

     > 1000 40 DUMP

     03E8 03C0 03EC 45E0 A220 0003 4520 A390 07FA *...Ж..s..,..TM.3*

     03F8 04C5 D4C9 E300 03E4 0402 9180 A331 4780 *.EMIT..U..JЦT..B*

     0408 A414 45E0 A230 0004 4520 A37A 9180 A330 *U...S.....T:JЦT.* OК

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


2

2.3. Стек возвратов и реализация структур управления

Один из важных принципов языка Форт состоит в том, чтобы предоставить программисту максимальный доступ ко всем средствам его реализации. В соответствии с этим принципом собственные данные адресного интерпретатора - стек возвратов - были сделаны доступными, для чего введены специальные слова: >R A -->, R> --> А и R@ --> A. Буква R (от RETURN - возврат) в имени этих слов напоминает о том, что они работают со стеком возвратов. Все эти слова можно использовать только внутри компилируемых определений. Во время исполнения любого такого определения, представленного в шитом коде как подпрограмма верхнего уровня, на вершине стека возвратов находится адрес того места, откуда данное определение было вызвано (адрес возврата). Это значение было помещено туда действием CALL при входе в данное определение. Адрес возврата будет снят со стека возвратов и использован для продолжения интерпретации действием RETURN, составляющим семантику слова EXIT.

Перечисленные слова позволяют программисту вмешиваться в описанный механизм вызова, реализуя свои собственные схемы передач управления, и, кроме того, использовать стек возвратов для хранения временных данных в пределах одного определения (все положенные на стек возвратов значения должны быть сняты с него перед выходом из определения). Однако неосторожное манипулирование стеком возврата может вызвать непредсказуемые последствия, вплоть до разрушения форт-системы. Слово >R (читается "на эр") переносит значения с вершины стека данных на стек возвратов. Обратное действие выполняет слово R> (читается "с эр"). Слово R@ (читается "эр разыменовать") копирует вершину стека возвратов на стек данных, сохраняя это значение на стеке возвратов.

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

    : LIT ( --> A )  R@  @  R>  2+  >R  ;

Во время работы данного определений слово R@ кладет на стек адрес возврата, указывающий в этот момент на следующий после адреса статьи LIT элемент в интерпретируемой последовательности адресов (см. рис. 2.4). Слово @ разыменовывает этот адрес, таким образом на стеке оказывается скомпилированное в шитый код число. Слова R> 2+ >R увеличивают адрес возврата на 2, чтобы обойдя значение числа, т. е. чтобы оно не было воспринято как адрес некоторой статьи. Таким образом, в приведенном определении слова реализован интересный способ передачи параметра через адрес возврата, невозможный в традиционных языкам программирования.

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

    : COMPILE  ( ---> )  R@  @  , R>  2+  >R  ;

    : BRANCH   ( ---> )  R>  @  >R  ;

    : ?BRANCH  ( A  ---> )  R>  SWAP  IF  2+  ELSE  @  THEN  >R  ;

Слово COMPILE (компилировать) компилирует (т. е. добавляет) на вершину словаря значение, находящееся в шитом коде непосредственно за данной ссылкой на статью COMPILE. Слово BRANCH (переход) переустанавливает указатель интерпретации по адресу, скомпилированному вслед за дайной ссылкой на статью BRANCH . Наконец, слово ?BRANCH снимает значение со стека и анализирует его: если это ЛОЖЬ (нуль), то оно работает, как BRANCH, - указатель интерпретации устанавливается по адресу, скомпилированному вслед за данной ссылкой на статью ?ВRANCH, а если это ИСТИНА (не нуль), то при интерпретации данной последовательности скомпилированный адрес перехода будет обойден. То обстоятельство, что в определении слова ?BRANCH используется условный оператор, для реализации условного оператора несущественно, потому что на практике слова BRANCH и ?BRANCH реализуются в форт-системах как подпрограммы нижнего уровня. Приведенные определения лишь иллюстрируют выразительные возможности языка, показывая, что даже такие, самые элементарные действия можно задать машинно-независимым способом в виде высокоуровневых определений.

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

    : LITERAL  ( A ---> A/ )  STATE @

        IF  COMPILE  LIT  ,  THEN  ; IMMEDIATE

Слово LITERAL анализирует текущее состояние текстового интерпретатора: если это компиляция, то компилирует значение, находящееся на вершине стека, как литерал в шитый код; в противном случае это значение остается на стеке.

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

    : IF ( ---> A )  COMPILE ?BRANCH  HERE  2 ALLOT ;  IMMEDIATE

    : THEN ( A ---> )  HERE  SWAP  !  ;  IMMEDIATE

    : ELSE ( A1 ---> A2 )  COMPILE  BRANCH  HERE

        2 ALLOT  HERE  ROT  ! ;  IMMEDIATE

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

На рис. 2.5 показана последовательность состояний словаря и стека в разные моменты обработки фрагмента A B IF C D THEN E F в теле определения через двоеточие. Эту обработку выполняет текстовый интерпретатор, находящийся в состоянии компиляции. В результате строится последовательность адресов словарных статей в соответствии с принятой техникой шитого кода.

Пусть слова A, B, ..., F являются обычными форт-словами, не имеющими признака немедленного исполнения. Тогда соответствующие им адреса будут компилироваться на вершину словаря.

Состояние 1 на рис. 2.5. соответствует моменту обработки непосредственно перед вводом слова IF. На вершине словаря скомпилированы адреса статей A и B и указатель вершины HERE указывает на следующий адрес. Слово IF имеет признак немедленного исполнения, поэтому будет не скомпилировано, а исполнено.

I II II IV V

: ... A B IF C D THEN E F ... ;

Словарь

│ ... │ │ ... │ │ ... │ │ ... │ │ ... │

├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤

│--> A │ │--> A │ │--> A │ │--> A │ │--> A │

├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤

│--> B │ │--> B │ │--> B │ │--> B │ │--> B │

└──────────┘ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤

│-->?BRANCH│ │-->?BRANCH│ │-->?BRANCH│ │-->?BRANCH│

├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤

┌>│ │┌>│ │┌─│─────■ │┌─│─────■ │

│ └──────────┘│ ├──────────┤│ ├──────────┤│ ├──────────┤

│┌──> │ │--> C ││ │--> C ││ │--> C │

││ │ ├──────────┤│ ├──────────┤│ ├──────────┤

││ │ │--> D ││ │--> D ││ │--> D │

││HERE │ └──────────┘│ └──────────┘│ ├──────────┤

│ │┌──> └───> └>│--> E │

│ ││ ┌──> ├──────────┤

│ ││ │ │--> F │

│ ││HERE │HERE └──────────┘

│ │ ┌───>

│ │ │

│ │ │HERE

│ │

│ ┌──────────┐│ ┌──────────┐

└─│─────■ │└─│─────■ │

┌──────────┐ ├──────────┤ ├──────────┤ ┌──────────┐ ┌──────────┐

│ ... │ │ ... │ │ ... │ │ ... │ │ ... │

Стек

I II III IV V

Рис. 2.5. Компиляция условного оператора

Состояние II показывает результат исполнения слова IF . На вершину словаря скомпилирована ссылка на статью ?BRANCH , вслед за которой отведено еще 2 байта под адрес перехода, и адрес зарезервированного места сохранен на стеке данных.

Дальнейшие слова C и D будут опять компилироваться. Состояние III соответствует моменту перед вводом слова THEN .

Слово THEN, как и IF, имеет признак немедленного исполнения, поэтому будет исполняться; в результате возникнет состояние IV. В этот момент определяется адрес перехода для скомпилированной словом IF ссылки на статью ?BRANCH; это текущее значение указателя вершины словаря HERE, которое и вписывается в зарезервированные ранее 2 байта. Результат дальнейшей компиляции приводит к состоянию V.

Аналогичным образом исполняется и определение слова ELSE, которое компилирует обход части "иначе" и вписывает адрес ее начала в качестве адреса перехода для ссылки ?BRANCH. В свою очередь, адрес перехода для скомпилированного обхода будет вписан завершающим условный оператор словом THEN.

Можно повысить надежность программирования условного оператора введением контроля за соответствием слов IF, THEN и ELSE с помощью вспомогательного слова ?PAIRS :

    : ?PAIRS ( A1,A2 ---> )  - ABORT" НЕПАРНЫЕ СКОБКИ" ;

которое снимает два значения со стека и, если они не равны между собой (их разность не нуль), выдает сообщение об ошибке с пояснительным текстом "Непарные скобки". Стандартное слово ABORT" (выброс и кавычка) А --> (исполнение) по своему употреблению аналогично слову ." (точка и кавычка): оно снимает значение со стека и, рассматривая его как логическое, сигнализирует об ошибке, печатая следующий за ним текст до кавычки, если это значение ИСТИНА (не нуль). Усиленные таким контролем определения слов условного оператора выглядят следующим образом:

    : IF ( ---> A,1 )

        COMPILE ?BRANCH  HERE  2 ALLOT ;  IMMEDIATE

    : THEN ( A,1 ---> )  1  ?PAIRS  HERE  SWAP  ! ;  IMMEDIATE

    : ELSE ( A1,1 ---> A2,1 )  1  ?PAIRS

        COMPILE  BRANCH  HERE  2  ALLOT

        HERE  ROT  ! ;  IMMEDIATE

В этих определениях для контроля вместе с адресом зарезервированного места передается число 1, которое проверяется с помощью слова ?PAIRS в словах, использующих переданный адрес. Такой простой способ контроля на практике оказывается вполне достаточным. При этом программист может встроить любой другой контроль по своему желанию.

Приведенное определение условного оператора связано с реализацией стандартных слов BRANCH и ?BRANCH, выполняющих переходы в шитом коде. Из соображений эффективности эти слова обычно задают как подпрограммы нижнего уровня в машинном языке. Тогда в зависимости от архитектуры ЭВМ может оказаться предпочтительней не абсолютный, как в приведенной реализации, а относительный адрес перехода, компилируемый в шитый код сразу после ссылки на статью BRANCH или ?BRANCH. Чтобы сделать определения, использующие эти слова, машинно-независимыми, стандарт предусматривает следующие слова для организации таких ссылок:

    >MARK      ---> A

    >RESOLVE   A --->

    <MARK      ---> A

    <RESOLVE   A --->

Слово >MARK (от MARK - отметить) резервирует место для ссылки вперед в оставляет адрес зарезервированного места на стеке. Слово >RESOLVE (от RESOLVE - разрешить) снимает этот адрес со стека и вписывает в него ссылку на текущую вершину словаря в соответствии с принятой реализацией переходов в шитом коде, согласованной с реализацией слое BRANCH и ?BRANCH. Аналогично слова <MARK и <RESOLVE предназначены для организации ссылок назад. Слово <MARK кладет на стек текущий адрес вершины словаря, а слово <RESOLVE компилирует ссылку на переданную точку. Окончательно определения слов условного оператора можно задать следующим образом (как они и выглядят в большинстве практических реализаций):

    : IF ( ---> A,1 )  COMPILE ?BRANCH  >MARK  1 ;  IMMEDIATE

    : THEN ( A,1 ---> )  1  ?PAIRS  >RESOLVE ;  IMMEDIATE

    : ELSE ( A1,1 ---> A2,1 )  1  ?PAIRS  COMPILE  BRANCH

        >MARK  SWAP  >RESOLVE  1 ;  IMMEDIATE

Аналогичным образом реализуются слова для циклов с проверкой (рис. 2.6):

    : BEGIN ( ---> A,2 )  <MARK  2 ;  IMMEDIATE

    : UNTIL ( A1,2 ---> )  2  ?PAIRS

        COMPILE  ?BRANCH  <RESOLVE ;  IMMEDIATE

    : WHILE ( A1,2 ---> A1,A2,3 )  2 ?PAIRS

        COMPILE  ?BRANCH  >MARK  3 ;  IMMEDIATE

    : REPEAT ( A1,A2,3 ---> )  3  ?PAIRS  COMPILE  BRANCH

        SWAP  <RESOLVE  >RESOLVE ;  IMMEDIATE

Очевидно, что реализованные таким образом стандартные структуры управления могут произвольно глубоко вкладываться друг в друга; несоответствие скобочных структур будет немедленно замечено, и программист получит сообщение об ошибке.

I - ... A B BEGIN C D UNTIL E F ...

II - ... A B BEGIN C D WHILE E F REPEAT G H ...

III - ... A B DO C D LOOP E F ...

│ ... │ │ ... │ │ ... │

├────────────┤ ├────────────┤ ├────────────┤

│--> A │ │--> A │ │--> A │

├────────────┤ ├────────────┤ ├────────────┤

│--> B │ │--> B │ │--> B │

├────────────┤ ├────────────┤ ├────────────┤

┌─>│--> C │ ┌─>│--> C │ │--> (DO) │

│ ├────────────┤ │ ├────────────┤ ├────────────┤

│ │--> D │ │ │--> D │ │ ■─────│──┐

│ ├────────────┤ │ ├────────────┤ ├────────────┤ │

│ │--> ?BRANCH │ │ │--> ?BRANCH │ ┌─>│--> C │ │

│ ├────────────┤ │ ├────────────┤ │ ├────────────┤ │

└──│──────■ │ │ │--> ■─────│──┐ │ │--> D │ │

├────────────┤ │ ├────────────┤ │ │ ├────────────┤ │

│--> E │ │ │--> E │ │ │ │--> (LOOP) │ │

├────────────┤ │ ├────────────┤ │ │ ├────────────┤ │

│--> F │ │ │--> F │ │ └──│──────■ │ │

├────────────┤ │ ├────────────┤ │ ├────────────┤ │

│ ... │ │ │--> BRANCH │ │ │--> E │<─┘

│ ├────────────┤ │ ├────────────┤

└──│──────■ │ │ │--> F │

├────────────┤ │ ├────────────┤

│--> G │<─┘ │ ... │

├────────────┤

│--> H │

├────────────┤

│ ... │

I II III

Рис. 2.6. Компиляция циклов

Циклы со счетчиком (рис. 2.6) реализуются аналогичным образом через вспомогательные слова (DO), (LOOP) и (+LOOP), компилируемые в шитый код вместе с адресом перехода:

    : DO ( ---> A1,A2,4 )  COMPILE (DO)

        >MARK  <MARK  4 ;  IMMEDIATE

    : LOOP ( A1,A2,4 ---> )  4 ?PAIRS  COMPILE (LOOP)

        <RESOLVE  >RESOLVE ;  IMMEDIATE

    : +LOOP ( A1,A2,4 ---> )  4 ?PAIRS  COMPILE (+LOOP)

        <RESOLVE  >RESOLVE ;  IMMEDIATE

Слово (DO), с которого начинается исполнение скомпилированного цикла со счетчиком, переносит на стек возвратов следующий за ним адрес конца цикла (он нужен для немедленного выхода из цикла по слову LEAVE) и параметры данного цикла-начальное и граничное значения счетчика, снимая их с вершины стека данных. Эти значения находятся на вершине стека возвратов в течение всего времени исполнения тела цикла.

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

Аналогично работает и слово (+LOOP), которое дополнительно снимает со стека данных значение шага цикла. Разумеется, реализация этих слов должна соответствовать принятому способу задания переходов в шитом коде. Для прямых адресов перехода соответствующие определения можно задать так:

    : (DO) ( A2:ГРАНИЧНОЕ,A1:НАЧАЛЬНОЕ ---> )

        R> ( A2,A1,R:ВОЗВРАТ)  DUP  @  >R ( ДЛЯ LEAVE)

        ROT  >R ( ГРАНИЧНОЕ)  SWAP  >R  ( НАЧАЛЬНОЕ)

        2+  >R   ( ОБОЙТИ АДРЕС В ШИТОМ КОДЕ) ) ;

    : (LOOP) ( ---> )

        R>  R>  R@ ( R:ВОЗВРАТ,I:ТЕКУЩЕЕ,A2:ГРАНИЧНОЕ)

        -  0  1.  D+  ( R,I-A2+1,F:ПРИЗНАК ЗАВЕРШЕНИЯ)

        IF  ( ЗАКОНЧИТЬ)

            DROP  R>  R>  2DROP  2+  ( ОБОЙТИ АДРЕС )

        ELSE ( ПРОДОЛЖИТЬ)

            R@  +  >R  ( НОВОЕ ЗНАЧЕНИЕ СЧЕТЧИКА)

            @  ( АДРЕС НАЧАЛА ТЕЛА ЦИКЛА)

        THEN  >R  ;

Определение (+LOOP) выглядит аналогично.

Во всех приведенных примерах доступ к адресу возврата в шитом коде осуществляется через стек возвратов из данного определения. Этот адрес модифицируется словами 2+ или @, тем самым обеспечивая обход скомпилированной ссылки или переход по ее значению. Слова I и LEAVE, которые используются внутри тела цикла для получения текущего значения счетчика и немедленного выхода из цикла, можно задать так:

    : I ( ---> A )  R>  R@  SWAP  >R ;

    : LEAVE ( --> )  R>  DROP  R>  DROP  R>  DROP ;

Для повышения быстродействия практические реализации языка Форт обычно определяют все эти слова, как и BRANCH, в виде подпрограмм нижнего уровня в машинном коде. В этом случае, например, слово I полностью эквивалентно слову R@.

Описанная реализация циклов со счетчиком через использование стека возвратов накладывает ограничения при программировании. Неосмотрительное включение слов >R, R>, R@ и EXIT в тело цикла со счетчиком может привести к непредсказуемому результату.

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


2

2.4. Управление поиском слов

Множество слов, "известных" форт-системе, хранится в словаре в виде одного или более цепных списков словарных статей, соединенных через поле связи. Порядок поиска статей в этих списках обратен порядку их включения в словарь по времени определения: статья последнего определенного слова находится в начале списка. Такой порядок делает естественным исключение слов из словаря с помощью слова FORGET: нужный список просто усекается до соответствующего места.

Являясь самостоятельной структурой данных, список слов имеет и соответствующее определяющее слово - VOCABULARY (словарик, список слов) -->, аналогичное слову VARIABLE. Оно выделяет из входной строки очередное слово и определяет его как новый список слов, например, VOCABULARY A.

Со списками слов тесно связаны две стандартные переменные CONTEXT (контекст) и CURRENT (текущий) и слово FORTH (Форт), обозначающее список, который состоит из всех стандартных слов и включает, в частности, само это слово.

Поиск каждого введенного слова начинается в списке, на который указывает переменная CONTEXT , затем в случае неудачи просматривается список - текущее значение переменной CURRENT. Стандарт требует, чтобы последним просмотренным списком всегда был список FORTH. Исполнение слова, обозначающего список, делает его текущим значением переменной CONTEXT , т. е. первым списком, который просматривается при поиске слов.

Стандартное слово DEFINITIONS (определения)

    : DEFINITIONS ( ---> )  CONTEXT  @  CURRENT  !  ;

устанавливает переменную CURRENT по текущему значению переменной CONTEXT, т.е. соответствующий список становится вторым на очереди для просмотра и одновременно тем списком, куда добавляются новые словарные статьи. Первоначально обе эти переменные установлены на один и тот же список FORTH. К этому состоянию приводит исполнение текста FORTH DEFINITIONS. Разумеется, в этом случае поиск слов будет состоять в однократном просмотре списка слов FORTH.

Приведенное выше описание определяет порядок поиска лишь в общих чертах. Детали стандарт оставляет на усмотрение разработчиков реализации. Обычно используется схема, приведенная на рис. 2.7. Для каждого списка в поте параметров его статьи строится заголовок "фиктивной" статьи для невозможного слова (например, состоящего из одного пробела), и представлением списка (значением переменных CONTEXT и CURRENT ) служит адрес поля связи этого заголовка, т.е. адрес второго элемента в поле параметров. Сам этот элемент является входом в цепной список словарных статей, принадлежащих данному списку слов. В качестве его начального значения в момент создания списка словом VOCABULARY используется адрес поля параметров словарной статьи другого списка, который таким образом является базовым для данного (обычно это текущее значение переменной CONTEXT).

VOC-LINK CONTEXT CURRENT

┌───────────┐ ┌───────────┐ ┌───────────┐

│ ■ │ │ ■ │ │ ■ │

└───────────┘ └───────────┘ └───────────┘

│ │ │

│ A │ B │ FORTH

│ ┌─────┬─────┐ │ ┌─────┬─────┐ │ ┌─────┬─────┐

│ │ 1 │ "_" │ │┌──>│ 1 │ "_" │ │┌──>│ 1 │ "_" │

│ ├─────┴─────┤ ││ ├─────┴─────┤ ││ ├─────┴─────┤

│┌──│─────■ │<─┘│┌──│─────■ │<─┘│┌──│─────■ │

││ ├───────────┤ ││ ├───────────┤ ││ ├───────────┤

└──>│ ■─────│───│──>│ ■─────│───│──>│ │

│ └───────────┘ ││ └───────────┘ ││ └───────────┘

│ ││ ││

│ ││ ││

│ ││ ││

│ ┌───────────┐ ││ ┌───────────┐ ││ ┌───────────┐

└─>│ A1 │ │└─>│ B1 │ │└─>│ F1 │

├───────────┤ │ ├───────────┤ │ ├───────────┤

┌──│─────■ │ │┌──│─────■ │ │┌──│─────■ │

│ └───────────┘ ││ └───────────┘ ││ └───────────┘

│ ││ ││

└─> ... │└─> ... │└─> ...

┌── │┌── │┌──

│ ││ ││

│ ┌───────────┐ ││ ┌───────────┐ ││ ┌───────────┐

└─>│ Aj │ │└─>│ Bm │ │└─>│ Fn │

├───────────┤ │ ├───────────┤ │ ├───────────┤

│ ■─────│───┘ │ ■─────│───┘ │ │

└───────────┘ └───────────┘ └───────────┘

Рис 2.7. Структура списков словарных статей

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

В конечном счете любой список заканчивается словами списка FORTH, который уже не имеет базового. На рис. 2.7 показаны поля параметров списков A, B и FORTH. Список B является базовым для A и, в свою очередь, базируется на списке FORTH. Слова Ai, ... , A входят в список A, Bi, ... , Bm - в B и F1, ... , Fn - в FORTH.

Показанный на рисунке порядок поиска слов соответствует исполнению текста B DEFINITIONS A, в результате чего сначала будут просмотрены списки A, B, FORTH, а затем B, FORTH.

Чтобы обеспечить большую гибкость в управлении порядком поиска слов и иметь возможность переопределять стандартные слова, в некоторых реализациях предусмотрено создание безбазовых списков. Это списки, создаваемые в контексте списка FORTH, для которых FORTH был бы базовым. Наличие безбазовых списков компенсируется тем, что в алгоритме поиска последним действием после просмотра списков, определяемых переменными CONTEXT и CURRENT, просматривается список FORTH, если он еще не был просмотрен через эти переменные.

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

    : VOCABULARY ( ---> )  CREATE

        256  BL  +  ,  CONTEXT  @  2-  ,

        HERE  VOC-LINK  @  ,  VOC-LINK  !

        DOES>  ( A ---> )  2+  CONTEXT  !  ;

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

    : LATEST ( ---> NFA )  CURRENT  @  @  ;

которое возвращает адрес заголовка последней созданной словарной статьи (обычно это адрес поля имени). Через это слово становится очевидной, например, реализация слова IMMEDIATE:

    : IMMEDIATE ( --> LATEST Cl 128 OR LATEST С! >

которое устанавливает в единицу признак немедленного исполнения в байте-счетчике последней созданной словарной статьи.

В соответствии с общими принципами языка Форт сам процесс поиска слова в словаре доступен программисту. Стандарт предусматривает для этого следующие слова:

    '            ---> CFA

    [']          ---> CFA (испонение)

    [COMPILE]    --->   (КОМПИЛЯЦИЯ)

    FIND       A ---> CFA,N / A,0

Слово ' (апостроф, читается "штрих") вводит очередное слово и ищет его в словаре, возвращая адрес поля кода найденной статьи (если слово не найдено, то это считается ошибкой).

Слово ['] имеет признак немедленного исполнения и используется внутри определений через двоеточие, образуя вместе со следующим словом единую пару: во время исполнения адрес поля кода этого слова будет положен на стек данных.

Слово [COMPILE], уже встретившееся ранее, вводит и компилирует следующее слово независимо от признака немедленного исполнения.

Наконец, слово FIND (найти) позволяет формировать образец для поиска программным путем: его параметром является адрес строки со счетчиком, которая рассматривается как имя слова. В случае успеха FIND возвращает адрес поля кода его словарной статьи и значение N, характеризующее признак немедленного исполнения: 1, если признак установлен, и -1, если отсутствует. В случае неудачи слово FIND возвращает прежний адрес строки со счетчиком и значение 0 - сигнал о неудаче.

Разумеется, слова ' и ['] используют слово FIND :

    : '  ( ---> A )  BL  WORD  FIND  IF  EXIT  THEN

        COUNT  TYPE  -1  ABORT" ?"  ;

    : ['] ( ---> )   '  COMPILE  LIT  ,  ;  IMMEDIATE

    : [COMPILE] ( ---> )   '  ,  ;  IMMEDIATE

которое, таким образом, является основным в определении порядка поиска слов.

В заключение рассмотрим определение стандартного слова FORGET, которое вводит очередное слово, ищет его в словаре и исключает из словаря вместе со всеми словами, определенными после него.

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

    : FORGET ( ---> )     >NAME ( NFA)

        DUP  FENCE  @  U<  ABORT" ЗАЩИТА ПО FENCE"

        >R  VOC-LINK  !  ( NO:ВХОД В СПИСОК СПИСКОВ)

        BEGIN R@ OVER U< WHILE ( N:ЗВЕНО СВЯЗИ СПИСКА)

        FORTH  DEFINITIONS

        @  DUP  VOC-LINK ! ( N1:ЗВЕНО СЛЕДУЩЕГО)

        REPEAT  ( N1:ЗВЕНО ОСТАВШЕГОСЯ СПИСКА)

        BEGIN  DUP  4  - ( N:ЗВЕНО СВЯЗИ,L:ВХОД В СЛОВА)

        BEGIN  N>LINK  @  ( N,NFA:ОЧЕРЕДНОЕ СЛОВО)

        DUP  R@  U<  UNTIL  ( N,NFA:ОСТАЮЩЕЕСЯ СЛОВО)

        OVER 2- ( N,NFA1,L:АДРЕС СВЯЗИ ФИКТИВНОЙ СТАТЬИ)

        !  @  ?DUP  0=

        UNTIL  R>  ( NFA0)  HERE  -  ALLOT ;

Сначала определяется адрес статьи исключаемого слова, проверяется, что она расположена выше границы защиты, и этот адрес сохраняется на стеке возвратов. В ходе исполнения первого цикла перебираются все существующие статьи для списков слов и исключаются те, адреса которых оказались больше данного (в силу монотонного убывания адресов в цепном списке. VOC-LINK цикл прекращается, как только адрес очередной статьи оказывается меньше данного). Во время исполнения второго цикла продолжается перебор оставшихся статей для списков слов, при этом в каждом списке отсекаются статьи, адреса которых больше данного (здесь опять используется монотонное убывание адресов словарных статей в пределах одного списка). В заключение указатель вершины словаря понижается до заданного адреса, тем самым освобождая память в словаре. Перед началом описанных действий переменные CONTEXT и CURRENT устанавливаются на список FORTH, чтобы их значение было осмыслено во все время дальнейшей работы.


2

2.5. Реализация определяющих слов

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

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

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

: CONST CREATE , DOES> @ ;

4 CONST ХОР

5 CONST ОТЛ

/│\ /│\ /│\

┌──>┌─────┬─────┐ │ ┌─────┬─────┐ │ ┌─────┬─────┐ │

│ │ 3 │ "Х" │ │ Поле │ 5 │ "C" │ │ │ 1 │ ":" │ │

│ ├─────┼─────┤ │ имени ├─────┼─────┤ │ ├─────┴─────┤ │

│ │ "О" │ "Р" │ │ │ "O" │ "N" │ │ │ ■─────│──┘

│ ├─────┴─────┤ │ ├─────┼─────┤ │ ├───────────┤

│ │ ■─────│──┘ │ "S" │ "T" │ │ ┌──│─────■ │

│ ├───────────┤ Поле ├─────┴─────┤ │ │ ├───────────┤

│ │ ■─────│──┐ связи │ ■─────│──┘ │ │--> CREATE │

│ ├───────────┤ │ ├───────────┤ │ ├───────────┤

│ │ 4 │ │ Поле │ ■─────│──┐ │ │--> ] │

│ └───────────┘ │ кода ├───────────┤ │ │ ├───────────┤

│ │ Поле │--> CREATE │ │ │ │--> (;CODE)│

│ │ пара- ├───────────┤ │ └─>├───────────┤

│ ┌─────┬─────┐ │ метров│--> , │ └─────>│ │

│ │ 3 │ "О" │ │ ├───────────┤ CALL:│ RPUSH(RI) │

│ ├─────┼─────┤ │ │--> (;CODE)│ │ RI:=W+2 │

│ │ "Т" │ "Л" │ └───────────>├───────────┤ │ goto NEXT │

│ ├─────┴─────┤ ┌───────────>│ JSR DOES │ └───────────┘

└───│─────■ │ │ ├───────────┤

├───────────┤ │ │--> @ │

│ ■─────│──┘ ├───────────┤

├───────────┤ │--> EXIT │

│ 5 │ └───────────┘

└───────────┘

DOES: PUSH(W+2); PRUSH(RI);

RI:=RETJSR; goto NEXT;

Рис. 2.8. Структура статьи определяющих и определяемых слов

На рис. 2.8 приведена структура словарных статей, возникающая после обработки определений

    : CONST  CREATE  ,  DOES>  @  ;

    4  CONST  ХОР    5  CONST  ОТЛ

Там же показана словарная статья для слова : , в теле которой находится точка CALL адресного интерпретатора.

Поле кода статьи CONST содержит адрес точки CALL, а поле параметров содержит последовательность ссылок на словарные статьи, скомпилированную адресным интерпретатором в соответствии с техникой шитого кода. Слово DOES>, входящее в это определение, имеет признак немедленного исполнения, поэтому оно не компилируется вслед за ссылкой на статью для запятой, а исполняется. Его исполнение состоит в компиляции ссылки на статью вспомогательного слова (;CODE) и компиляции машинной команды JSR перехода с возвратом на специальную точку DOES в ядре форт-системы. Далее текстовый интерпретатор компилирует ссылку на статью для @ и исполняет (в силу признака немедленного исполнения) слово ;, которое завершает построение словарной статьи. Оно компилирует ссылку на статью EXIT и переключает текстовый интерпретатор в состояние исполнения.

Во время исполнения слова CONST во фрагменте 4 CONST ХОР входящее в него слово CREATE создаст для слова ХОР заголовок словарной статьи и поле кода, заслав туда стандартный адрес. Следующее слово, скомпилирует число 4 в поле параметров, а слово (;CODE) занесет в поле кода адрес следующей за ним машинной программы и закончит исполнение слова CONST:

    : (;CODE) ( ---> )  R>  LATEST  NAME>  !  ;

Значение, снимаемое с вершины стека возвратов словом R>, - это как раз адрес команды JSR в определении слова CONST. Слово LATEST кладет на стек адрес заголовка последней созданной статьи, т. е. статьи ХОР, а слово NAME> преобразует этот адресов адрес поля кода. Поскольку со стека возвратов было снято одно значение, то по завершении данного определения управление вернется в точку после вызова определения, вызвавшего данное, т. е. на продолжение работы после вызова слова CONST. Аналогичные действия будут исполнены при обработке текста 5 CONST ОТЛ. Если теперь слово ХОР будет исполняться, то из точки NEXT адресного интерпретатора управление будет передано на машинную программу по адресу из поля кода, т. е. на команду JSR в теле определения CONST. Эта команда перехода с возвратом передает управление на точку DOES, сообщив ей в качестве своего адреса возврата адрес следующей последовательности ссылок - исполняющей части определения CONST . Точка DOES кладет на стек адрес поля параметров статьи ХОР (в этот момент в рабочей ячейке W еще находится адрес поля кода статьи ХОР, загруженный туда действием NEXT) и исполняет действие CALL для исполняющей части определения CONST. Следующее исполняемое слово @ заменит на стеке адрес поля параметров статьи ХОР числом 4, скомпилированным по этому адресу, и затем слово EXIT завершит исполнение данного вызова слова ХОР.

Слово : тоже является определяющим, и его словарная статья имеет такую же структуру. Рассмотрим его работу на примере трансляции определения CONST.

Создающая часть слова : состоит из слов CREATE и ]. Первое выбирает из входной строки следующее за двоеточием слово (в данном случае CONST) и создает для него начало словарной статьи (заголовок и поле кода), а второе переключает текстовый интерпретатор в состояние компиляции. Последнее в создающей части слово (;CODE) вписывает в поле кода создаваемой новой статьи текущее значение указателя интерпретации, т. е. адрес точки CALL, которая располагается в теле данного определения, после чего исполнение двоеточия заканчивается. Поскольку теперь интерпретатор находится в состоянии компиляции, то следующие вводимые слова будут компилироваться, заполняя поле параметров статьи CONST последовательностью ссылок. Так будет продолжаться до тех пор, пока слово - точка с запятой, отмечающее конец определения и имеющее признак немедленного исполнения, не переключит интерпретатор обратно в состояние исполнения:

    : ;  ( ---> )  COMPILE  EXIT  [COMPILE]  [  ;  IMMEDIATE

Определяющие слова являются механизмом для создания классов слов со сходным "поведением", которое определяется исполняющей частью и включается в словарную статью слова через поле кода. Разница между словами внутри одного класса состоит в значении поля параметров, которое строится при определении слова. Слова, определенные через двоеточие, составляют один из классов наряду с константами, переменными, списками слов и другими, которые программист может вводить по своему усмотрению. При этом достигается существенная экономия памяти за счет того, что общая часть всех слов одного класса присутствует в памяти в единственном экземпляре в статье определяющего слова.

Таким образом, в основе языка Форт лежат две структуры: внешняя - простейшая синтаксическая структура слова, представленного как последовательность литер, ограниченная пробелом, и внутренняя - структура словарной статьи, в которой поле кода задает интерпретатор поля параметров. Одним из частных случаев такого интерпретатора является адресный интерпретатор, и тогда поле параметров содержит последовательность интерпретируемых адресов в шитом коде. Этому случаю отвечает определение через двоеточие. Другие определяющие слова задают другие интерпретаторы и соответственно другие структуры поля параметров.


2

2.6 Встроенный ассемблер

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

Примеры обоих случаев дает сама форт-система. Очевидно, что слова, выполняющие арифметические операции или действия с вершиной стека, естественно реализовывать непосредственно в машинном коде, поскольку скорость их исполнения в значительной степени определяет быстродействие системы. Вместе с тем слово 1+, увеличивающее на единицу значение на вершине стека, можно определить как в машинном коде (особенно если в данной ЭВМ есть специальная команда увеличения значения на единицу), так и через двоеточие:

  : 1+   ( A ---> A+1 )  1  +  ;

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

Для определения слов непосредственно в машинном коде стандарт языка Форт предусматривает следующие слова:

    ASSEMBLER  --->

    CODE       --->

    END-CODE   --->

    ;CODE      ---> (компиляция)

составляющие стандартное ассемблерное расширение обязательного набора слов.

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

Слово CODE (код) является определяющим для слов нижнего уровня и обычно определяется так:

    : CODE  ( ---> )

        CREATE  HERE  LATEST  NAME>  !  ASSEMBLER  ;

Оно используется в сочетании со словом END-CODE (конец кода): CODE <имя> <машинный-код> END-CODE, где "имя" является именем определяемого слова, а "машинный-код" - записью его реализации в машинном коде в соответствии с принятыми соглашениями.

Поле кода такой словарной статьи содержит адрес ее поля параметров, в котором располагается данный машинный код.

Наконец, слово ;CODE, имеющее признак немедленного исполнения, позволяет задавать исполняющую часть определяющих слов непосредственно в машинном коде:

    : ;CODE ( ---> )  COMPILE  (;CODE)

        [COMPILE] [ ASSEMBLER ; IMMEDIATE

Оно используется внутри определения через двоеточие для определяющего слова аналогично слову DOES>

    : <имя>  <создающая-часть>

        ;CODE  <машинный-код>  END-CODE

и отделяет высокоуровневую создающую часть от исполняющей части, заданной в машинном коде. Во время исполнения скомпилированного словом ;CODE слова (;CODE) адрес машинной программы, составляющей исполняющую часть, будет заслан в поле кода определяемого слова, которое таким образом получит интерпретатор, реализованный в машинном коде. На практике именно таким способом задают стандартные определяющие слова :, CONSTANT и VARIABLE.

Конкретный вид машинной программы зависит от архитектуры данной ЭВМ. Общим правилом является то, что этот текст представляет собой последовательность слов, которые исполняются текстовым интерпретатором, в результате чего на вершине словаря формируется соответствующий двоичный машинный код. Машинные команды записываются в естественной для Форта обратной польской форме: сначала операнды, а затем слово, обозначающее мнемонику команды.

Операнды - это слова, вычисляющие на стеке размещения операндов: номера регистров, адреса в памяти и их модификации, значения непосредственных операндов и т.д.

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

Включение запятой в имя слова для кода команды, с одной стороны, подчеркивает тот факт, что данное слово компилирует команду, а с другой стороны, позволяет отличать, например, часто встречающиеся мнемоники ADD, СН и т.д. от чисел, заданных в шестнадцатиричной системе.

В табл. 2.1 приведена запись одних и тех же машинных команд в традиционном ассемблере и встроенном ассемблере разных форт-систем для нескольких распространенных типов ЭВМ. Как и в случае реализации структур управления, во встроенный ассемблер можно включить дополнительные средства контроля, которые, учитывая формат машинных команд, проверяют правильность задания их операндов.

Таблица 2.1. Сравнительная запись машинных команд в традиционном

ассемблере и встроенном ассемблере форт-системы

┌─────────┬─────────────────────────┬───────────────────────────────────┐

│ Тип ЭВМ │ Традиционный ассемблер │ Встроенный ассемблер форт-системы │

├─────────┼─────────────────────────┼───────────────────────────────────┤

│ СМ-4 │ CMPB #12, (R1)+ │ 12# R1 )+ CMPB, │

│ │ JMP NEXT │ NEXT JMP, │

│ │ RTS │ RTS, │

├─────────┼─────────────────────────┼───────────────────────────────────┤

│ ЕС ЭВМ │ STM 14,12,12(13) │ 14 12 12 (, 13 STM, │

│ │ BALR 15, 0 │ 15 0 BALR, │

│ │ B NEXT │ NEXT B, │

├─────────┼─────────────────────────┼───────────────────────────────────┤

│ │ MOV A, B │ A B MOV, │

│ К580 │ LXI H, 15 │ H 15 LXI, │

│ │ POP H │ H POP, │

├─────────┼─────────────────────────┼───────────────────────────────────┤

│ │ , UTC, =15 │ 0 0 5 # # UTC, │

│ БЭСМ-6 │ . XTA, │ 0 0 XTA, │

│ │ 3, UTC, 777 │ 3 777 UTC, │

└─────────┴─────────────────────────┴───────────────────────────────────┘

В гл. 3 рассмотрена реализация полного встроенного ассемблера для микропроцессора К580, занимающая 100 строк текста на языке Форт.

Встроенный ассемблер форт-систем часто делают "структурным", т.е. включают в него операторы ветвления и циклы, выполняющие переходы по значению управляющих разрядов в специальном регистре. По аналогии с такими же средствами языка Форт эти структуры задают с помощью тех же слов с добавлением, запятой: IF, THEN, ELSE, BEGIN, UNTIL и т.д. При этом вводят слова, обозначающие различные состояния управляющих сигналов, а слова, реализующие структурные операторы компилируют команды переходов, соответствующие указанным состояниям. Такой подход позволяет во многих случаях избежать введения сложного механизма переходов на метку, поскольку ассемблерные вставки, для трансляции которых и существует встроенный ассемблер, состоят, как правило, из нескольких команд.

Программа в машинном коде, заданная через слова CODE или ;CODE, получает управление от адресного интерпретатора и после ее завершения возвращает управление интерпретатору на точку NEXT. Для связи с адресным интерпретатором в списке слов ASSEMBLER обычно предусматривается ряд констант, обозначающих точки входа, номера регистров и другие ресурсы адресного интерпретатора. Аналогичным образом предусматривается связь с операционной системой, встроенным постоянным программным обеспечением и аппаратурой.

В общем объеме больших программ на Форте ассемблерные коды составляют незначительную часть (в реализации самой форт-системы, например, 10-30%), но вместе с тем такая возможность делает программиста максимально свободным в выборе средств для реализации своих идей.


2

2.7 Работа с внешней памятью

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

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

В языке Форт применяется механизм виртуальной внешней памяти, состоящей из блоков по 1 К байт каждый. Блок идентифицируется номером - числом в диапазоне от 1 до 32767. В адресном пространстве форт-системы выделяется память под буферный пул, рассчитанный на одновременное хранение одного или более блоков. Каждый буфер помимо памяти для хранения собственно блока данных содержит номер блока и признак его измененности.

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

Слово BLOCK (блок) аналогично по использованию слову BUFFER за исключением того, что перепись данных из внешней памяти производится в приписанный данному блоку буфер. Разумеется, если данный блок уже находится в буферном пуле, то переписи данных не происходит, и слово BLOCK возвращает адрес этого буфера.

Наконец, слово UPDATE (изменить) устанавливает признак измененности последнего блока, к которому было адресовано обращение через слово BLOCK или BUFFER. Таким образом, впоследствии этот блок будет автоматически переписан во внешнюю память.

При реализации обмена с внешней памятью в качестве буферного пула обычно используется связный участок оперативный памяти. Пусть его начало задается константой FIRST, а конец - адрес байта, следующего за последним,- константой LIMIT. (Если пул располагается вплотную к концу адресного пространства, то этот следующий адрес равен нулю!) Буфера в пуле располагаются подряд, каждый начинается двух-байтной ячейкой, в которой записывается номер приписанного блока, причем старший разряд используется под признак измененности. Далее идет буферная память для блока размером 1024 байта, завершается буфер еще одной служебной ячейкой, в которой записан ноль (ее назначение указано в п.2.8). Пусть переменные PREV и USE указывают на текущий используемый буфер и следующий, который будет выдан при запросе на свободный буфер. Определим слово +BUF, которое вычисляет адрес буфера, следующего в пуле за переданным, и возвращает признак несовпадения его с текущим:

    : +BUF ( A1 ---> A2,F )  1024  +  4  +

        DUP  LIMIT  -  IF  DROP  FIRST  THEN

        DUP  PREV  @  -  ;

Пусть служебные слова RBLK A,N --> и WBLK A,N --> выполняют чтение блока с указанным номером в заданную область оперативной памяти и запись из нее. Тогда с учетом принятых условий слова, выполняющие работу с внешней памятью, можно задать так:

          : BUFFER ( N ---> A  ПРИПИСАТЬ БЛОКУ N БУФЕР)

            USE  @  DUP  >R  ( ВЕРНЕМ ЭТОТ БУФЕР)

            BEGIN +BUF UNTIL  USE ! ( УСТАНАВЛ.СЛЕДУЮЩИЙ)

            R@  @  0<  ( ПРИЗНАК ИЗМЕНЕННОСТИ?)

            IF  R@  2+  R@  @  32767  AND  WBLK  THEN

            R@  !  ( ПРИПИСАЛИ НОВОМУ БЛОКУ)

            R@  PREV  !  R>  2+  ;

          : BLOCK ( N ---> A:АДРЕС БУФЕРА С ДАННЫМИ БЛОКА)

            >R PREV @ DUP R@ - DUP + ( ТЕКУЩИЙ - ТОТ ЖЕ?)

            IF ( НЕТ)  BEGIN +BUF  0=

             IF  DROP  R@  BUFFER DUP R@  RBLK  2- THEN

             DUP @  R@  -  DUP  +  0= UNTIL  DUP  PREV !

            THEN  R>  DROP  2+  ;

          : UPDATE ( ---> )  PREV @ @  32768 OR  PREV @ ! ;

Для записи всех измененных буферов во внешнюю память служит слово SAVE-BUFFERS (сохранить буфера):

          : SAVE-BUFFERS ( ---> )

            LIMIT  FIRST  DO  I  @  32768  AND

               IF  I  @  32767  AND  DUP  I  !

               I  2+  SWAP  WBLK  THEN

            1028  +LOOP  ;

При исполнении слова SAVE-BUFFERS все буфера остаются приписанными прежним блокам. Слово FLUSH (очистить) переписывает все исправленные блоки во внешнюю память и освобождает буфера. Многие реализации имеют слово EMPTY-BUFFERS (опустошить буфера), которое освобождает буферный пул, не переписывая исправленные блоки:

          : EMPTY-BUFFERS ( ---> )

            LIMIT  FIRST  DO  0  I  @  !  1028  +LOOP  ;

          : FLUSH ( ---> )  SAVE-BUFFERS  EMPTY-BUFFERS  ;

Внешняя память форт-системы в основном используется для хранения форт-текстов. Блок внешней памяти с форт-текстом называется экраном и условно разбивается на 16 строк по 64 литеры в каждой. Такой формат экрана сложился традиционно и закреплен в стандарте.

Программист создает и исправляет экраны во внешней памяти, используя встроенный редактор форт-системы. Как и в случае встроенного ассемблера, стандарт не определяет конкретный вид и состав его команд, поскольку это в значительной степени определяется функциональными возможностями и характеристиками конкретных терминалов и средствами работы с ними. Обычно слова-команды текстового редактора составляют отдельный список слов с именем EDITOR (редактор), базирующийся на списке FORTH. Для распечатки редактируемого экрана на терминале чаще всего используют слово LIST (распечатать), которое, кроме того, сохраняет номер экрана в служебной переменной SCR:

    : LIST ( N ---> ) DUP SCR !

        CR  ." ЭКРАН "  DUP  .  BLOCK

        16  0  DO  DUP  I  64  *  +

        CR  I  3  .R   SPACE  64  TYPE  LOOP  DROP  ;

Вначале печатается номер данного экрана, затем его строки. Перед каждой строкой печатается ее номер - число в диапазоне от 0 до 15. По завершении редактирования исправленные экраны можно записать во внешнюю память словом FLUSH.


2

2.8. Интерпретация входного потока

Собственно работа форт-системы заключается в распознавании и исполнении слов-команд, которые программист вводит с терминала. Ввод осуществляется построчно: набрав нужный текст, программист нажимает специальную управляющую клавишу, сообщая тем самым о завершении ввода. Форт-система размещает введенный текст в специальном буфере для ввода с терминала, который располагается в адресном пространстве оперативной памяти. Адрес начала этого буфера дает стандартное слово TIB (сокращение от TEXT INPUT BUFFER - буфер для ввода текста), его длина хранится в стандартной переменной #TIB . Данный буфер представляет собой входной поток, из которого слово WORD выбирает слова. По исчерпании входного потока форт-система вводит в этот буфер новый текст, получаемый от программиста. При возникновении какой-либо ошибочной ситуации форт-система прекращает дальнейшую интерпретацию текущего содержимого этого буфера и, выдав соответствующее сообщение программисту, заполняет буфер новым текстом, который после этого вводит программист.

Альтернативой вводу текста с терминала является ввод из внешней памяти форт-системы. Переключением входного потока на внешнюю память и обратно на терминал управляет стандартная переменная BLK (сокращение от BLOCK-блок), значение которой проверяется каждый раз в слове WORD. Если это нуль, то в качестве входного потока служит буфер TIB, в противном случае это значение рассматривается как номер блока внешней памяти, который используется как входной поток (этот блок переносится в оперативную память словом BLOCK). Текущая позиция во входном потоке хранится в стандартной переменной >IN (от IN - вход) и в случае ввода с терминала изменяется в пределах от 0 до значения #TIB, а при вводе из внешней памяти - в диапазоне от 0 до 1024.

Обычно конец входного потока в оперативной памяти отмечается нулевым кодом (именно для этого в буферном пуле после памяти для данных блока резервируется еще одна ячейка). Слово WORD , "натыкаясь" на нулевой код, возвращает в качестве результата пустую строку с нулевым значением счетчика литер, при этом в список FORTH включается словарная статья для такого "пустого" слова. Оно имеет признак немедленного исполнения и поэтому всегда исполняется назависимо от текущего состояния текстового интерпретатора. Его исполнение состоит в прекращении интерпретации данного входного потока и тем самым позволяет избежать дополнительных проверок на исчерпание. Обычно в реализациях языка Форт определяется слово INTERPRET (интерпретировать), выполняющее интерпретацию текущего входного потока. Оно представляет собой бесконечный цикл по вводу и исполнению (или компиляции) слов:

    : INTERPRET ( ---> )  BEGIN  BL  WORD  FIND

        ?DUP  IF  ( ПРОВЕРИТЬ ПРИЗНАК IMMEDIATE)

        1+  IF  EXECUTE

            ELSE  STATE  @  IF  ,  ELSE  EXECUTE  THEN

            THEN

        ELSE  ( МОЖЕТ БЫТЬ ЭТО ЧИСЛО?)

            NUMBER  DPL  @  1+

            IF  [COMPILE]  2LITERAL

            ELSE  DROP  [COMPILE]  LITERAL  THEN

        THEN  AGAIN  ;

В приведенном примере конструкция BEGIN-AGAIN определяет бесконечный цикл.

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

В случае если очередное введенное слово не найдено в словаре, исполняется слово NUMBER, которое пытается воспринять его как запись числа в соответствии с текущим основанием системы счисления-значением переменной BASE. Если это удалось, то слово NUMBER возвращает значение числа как значение двойной длины и дополнительно в переменной DPL сообщает позицию десятичной точки в нем (-1, если точки в записи числа не было). В противном случае возникает ошибочная ситуация "Слово не найдено", и интерпретация прекращается. Если же введенное слово оказалось числом, то в зависимости от наличия в нем точки оно рассматривается как число двойной или одинарной точности. Таким образом, пустое слово-ограничитель входного потока-прекращает исполнение слова INTERPRET и возобновляет исполнение слова, его вызвавшего:

    : X ( ---> )  R>  DROP  ;  IMMEDIATE

Здесь X обозначает пустое слово.

Таким образом, работу форт-системы можно задать таким бесконечным циклом:

    : ФОРТ-СИСТЕМА ( ---> )

        BEGIN  CR  ." >"   ( ПРИГЛАШЕНИЕ К ВВОДУ)

            TIB  80  EXPECT ( ВВЕСТИ ТЕКСТ С ТЕРМИНАЛА)

            SPAN  @  #TIB  ! ( УСТАНОВИТЬ ЕГО ДЛИНУ)

            0 TIB  #TIB @ +  C! ( УСТАНОВИТЬ ОГРАНИЧИТЕЛЬ)

            0  >IN !  0  BLK ! ( УСТАНОВИТЬ ВХОДНОЙ ПОТОК)

            INTERPRET ( ИНТЕРПРЕТИРОВАТЬ ВВЕДЕННЫЙ ТЕКСТ)

            STATE @  0=  IF ." OK" THEN ( ПОДТВЕРЖДЕНИЕ)

        AGAIN  ;

Для переключения входного потока на внешнюю память стандарт предусматривает слово LOAD (загрузить) :

    : LOAD ( N:НОМЕР --->  ИНТЕРПРЕТИРОВАТЬ ЭКРАН)

        >IN  @  >R  BLK  @  >R  ( СОХРАНИТЬ ТЕКУЩИЙ)

        BLK  !  0  >IN  !  ( УСТАНОВИТЬ НОВЫЙ)

        INTERPRET  ( ПРОИНТЕРПРЕТИРОВАТЬ ЕГО)

        R> BLK !  R> >IN ! ; ( ВЕРНУТЬСЯ К ПРЕЖНЕМУ)

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

Некоторые реализации предусматривают слово THRU (сквозь) для последовательной интерпретации диапазона экранов:

    : THRU ( N1,N2 ---> ИНТЕРПРЕТИРОВАТЬ ЭКРАНЫ)

           (           ОТ N1 ДО N2 ВКЛЮЧИТЕЛЬНО)

           1+  SWAP  DO  I  LOAD  LOOP  ;

Для продолжения интерпретации следующего экрана часто используется слово -->, логически сцепляющее следующей экран с данным:

    : --> ( ---> ) BLK @ 0=

        ABORT" НЕДОПУСТИМОЕ ИСПОЛЬЗОВАНИЕ -->"

        0  >IN  !  BLK  @  1+  BLK  !  ; IMMEDIATE

В этом случае интерпретации сцепленных экранов нужно "загрузить" словом LOAD только первый из этих экранов.


3

3.2. Инфиксная запись формул

Для многих программистов трудным барьером на пути к овладению языком Форт оказывается используемая в нем обратная польская форма для записи выражений. Опишем простую надстройку над языком Форт, которая позволяет записывать формулы в обычной инфиксной форме с использованием скобок. Будем по-прежнему считать все элементы такого выражения (скобки, знаки операций и элементарные термы) отдельными форт-словами и разделять их при записи пробелами. Задача состоит в том, чтобы вычисления на стеке автоматически перегруппировывались исходя из инфиксной формы записи. Например, чтобы вместо текста 2 5 + 7 3 - * можно было писать ( ( 2 + 5 ) * ( 7 + 3) ). Внешние скобки нужны для того, чтобы отмечать конец выражения. При желании это можно задавать и каким-нибудь другим способом.

Для операций в инфиксной записи вводится понятие приоритета (старшинства), которое определяет порядок вычислений при отсутствии скобок. Приоритет обозначается целым числом, и операции с меньшим приоритетом выполняются после операций с большим приоритетом. Например, в выражении А+В/С подразумевается следующая расстановка скобок: (А+(В/С)), т.е. сначала выполняется деление и только потом сложение, потому что приоритет деления выше приоритета сложения. В случае равных приоритетов, например в выражении А+В+С, будем выполнять операции слева направо: ((А+В)+С). Традиционно используемые приоритеты двухместных операций показаны в табл. 3.1. Все одноместные операции (ABS, NEGATE, NOT и др.) имеют максимальный приоритет (обычно 9).

Таблица 3.1. Приоритеты двухместных операций

┌───────────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐

│ Приоритет │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │

├───────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤

│ Операция │ OR │ AND │ = │ < │ + │ * │ │

│ │ XOR │ │ │ > │ - │ / │ ** │

│ │ │ │ │ │ │ MOD │ │

└───────────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

Опишем вспомогательную структуру данных - стек ОПРЦ, элементами которого являются пары значений: приоритет операции и адрес кода, который ее вычисляет. Размер стека определяется максимальной глубиной вложенности формул, которую мы допускаем: CREATE ОПРЦ HERE 2 + , 40 ALLOT. Первым элементом в поле параметров слова ОПРЦ является указатель вершины этого стека (адрес первого свободного байта), далее зарезервирована память на 5 элементов по 4 байта (два значения) каждый. По аналогии со словами >R, R@ и R> определим слова для работы со стеком ОПРЦ:

    : >ОПРЦ ( A --->)  ОПРЦ  @  !  2  ОПРЦ +!  ;

    : ОПРЦ@ ( ---> A)  ОПРЦ  @  2-  @  ;

    : ОРПЦ> ( ---> A)  ОПРЦ@   -2  ОПРЦ +!  ;

Обработка операций в инфиксной форме состоит в том, что операция не выполняется, а помещается вместе со своим приоритетом на стек операций, выталкивая из него на исполнение все операции с меньшим или равным приоритетом. В состоянии исполнения вытолкнутая операция исполняется, а в состоянии компиляции - компилируется. Предполагая, что в стеке ОПРЦ сначала размещается адрес поля кода для исполнения операции, а за ним приоритет, определим слово для такого выталкивания операций:

    : >ОПРЦ> ( N:ПРИОРИТЕТ---> )  >R   BEGIN ОПРЦ@

        R@  <  NOT  WHILE  ОПРЦ>  DROP  ОПРЦ> ( CFA)

        STATE @ IF , ELSE EXECUTE THEN REPEAT R> DROP ;

Теперь введем определяющие слова для двухместных и одноместных операций и переопределим через них стандартные арифметические форт-слова:

    : 2-ОП ( N:ПРИОРИТЕТ->)   >IN @ >R ' ( N,CFA)

        R> >IN !  CREATE  IMMEDIATE ( N,CFA) , ,

        DOES>  ( PFA->) 2@  ( N,CFA)

        >R >R R@ >ОПРЦ> R> R> >ОПРЦ >ОПРЦ ;

    : 1-ОП ( ->) 9 2-ОП ;

    2 2-ОП OR   2 2-ОП XOR   3 2-ОП AND   4 2-ОП =

    5 2-ОП <    5 2-ОП >     6 2-ОП +     6 2-ОП -

    7 2-ОП *    7 2-ОП /     7 2-ОП MOD

    1-ОП NOT   1-ОП ABS   1-ОП NEGATE

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

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

    : ( 0 >ОПРЦ ; IMMEDIATE

    : > 1 >ОПРЦ> ОПРЦ> DROP ; IMMEDIATE

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

Развивая описанную надстройку дальше, определим простой входной язык с описаниями переменных и присваиваниями, которые записываются обычным образом. В качестве знака присваивания пусть используется слово :=, а в качестве разделителя операторов - слово ;. Если переменная использована как получатель присваивания (слева от знака :=), то ее исполнение оставляет на стеке адрес значения; а если данная переменная входит в правую часть присваивания, то ее исполнение кладет на стек само значение данной переменной. Для управления поведением переменных нашего языка введем рабочую переменную ?ЗНАЧ, которая имеет значение 0 при обработке левой части присваивания и значение -1 при обработке правой, и определим слово ПЕРЕМ для описания переменных нашего языка:

    VARIABLE ?3НАЧ

    : ПЕРЕМ CREATE 0 , DOES> [COMPILE] LITERAL

        ?ЗНАЧ @ IF STATE @ IF COMPILE @ ELSE @ THEN

        THEN ; IMMEDIATE

Для записи присваивании определим слова := и ; через уже определенные скобки:

    : := [COMPILE] ( -1 ?3НАЧ ! ; IMMEDIATE

    : ; [COMPILE] ) STATE @ IF COMPILE SWAP COMPILE !

        ELSE SWAP ! THEN  0 ?ЗНАЧ ! ;  IMMEDIATE

Слово := кладет на стек ОПРЦ ограничитель для выталкивания операций и устанавливает переменную ?ЗНАЧ на обработку правой части присваивания. Слово ; выталкивает со стека ОПРЦ все накопившиеся там операции, в результате на вершине стека данных оказывается значение правой части. Непосредственно под ним располагается адрес переменной, оставленный левой частью данного присваивания. Слова SWAP и ! выполняют присваивание, причем в состоянии компиляции они компилируются, а состоянии исполнения исполняются. В заключение переменная ?ЗНАЧ переустанавливается на режим обработки левой части следующего присваивания.

Благодаря словам := и ; отпадает необходимость в дополнительных внешних скобках для всего выражения. Входной текст в описанном расширении выглядит вполне традиционно:

    ПЕРЕМ A  ПЕРЕМ B

    А := 10 ;  B := 15 ;

    A := ( A + B ) * ( A - B ) + 2 ;

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


3

3.3. Локальные переменные

Стек данных как универсальное средство для передачи параметров и результатов между форт-словами имеет неоспоримые преимущества. Вместе с тем внутри определения он используется для промежуточных вычислений и размещения значений, которые в них участвуют. Это вызывает определенные трудности для доступа к такому локальному значению, поскольку его положение относительно вершины стека постоянно меняется.

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

Имеющийся в языке механизм описания переменных в данном случае не подходит, поскольку создает глобальные имена, тогда как требуется именовать локальные объекты, учитывая при этом возможность рекурсивных вызовов. Поставленную задачу решает включение в работу дополнительного стека, отличного от стека данных. Локальные значения размещаются в этом стеке при входе в определение и убираются из него при выходе.

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

Простейшая надстройка над языком Форт, которая позволяет работать с локальными переменными, выглядит так:

    100 ALLOT  HERE CONSTANT LP0 ( НАЧАЛО ЛОК.СТЕКА)

    VARIABLE LP  ( ТЕКУЩАЯ ВЕРШИНА ЛОКАЛЬНОГО СТЕКА)

    : INIT ( ->)  LP0 LP ! ; INIT

    : LOC ( N:СЧЕТЧИК->) 1+ 2+ LP @ OVER - DUP LP ! ! ;

    : UNLOC ( ->)  LP @ @ LP +! ;

    : @@ ( N:СМЕЩ->) CREATE , DOES> ( PFA->A) @ LP @ + @ ;

    : !! ( N:СМЕЩ->) CREATE , DOES> ( A,PFA->) @ LP @ + ! ;

    2 @@ @1  4 @@ @2  6 @@ @3  8 @@ @4  10 @@ @5 ( И Т.Д.)

    2 !! !1  4 !! !2  6 !! !3  8 !! !4  10 !! !5 ( И Т.Д.)

Вначале отводится область объемом 100 байт и адрес ее конца запоминается как константа LP0. Эта область будет использоваться как локальный стек, растущий в сторону убывания адресов. Переменная LP хранит указатель на текущую вершину локального стека, ее инициализацию выполняет слово INIT, которое присваивает ей значение LP0. Слово LOC резервирует в этом стеке память на N двухбайтных значений, дополнительно отводя еще одну ячейку, в которую засылает значение N - длину всей области. Обратное действие - освобождение памяти - выполняет слово UNLOC, которое использует сохраненное значение N. Слова @@ и !! являются определяющими для двух рядов слов, выполняющих разыменование локальных объектов и присваивание им нового значения. Каждый локальный объект в пределах одного определения идентифицируется своим номером, который включен в имя операций разыменования и присваивания для работы с ним. В качестве примера рассмотрим определение слова 5INV, переставляющего 5 верхних элементов стека данных в обратном порядке:

    : 5INV ( A,B,C,D,E->E,D,C,B,A)  5 LOC

        !1 !2 !3 !4 !5  @1 @2 @3 @4 @5  UNLOC ;

Приведенная реализация локальных переменных предельно упрощена. Ее очевидным усовершенствованием является, например, введение контроля за переполнением и исчерпанием локального стека и включение действий LOC и UNLOC в семантику входа в высокоуровневое определение и выхода из него; при этом подсчет числа локальных объектов может выполняться автоматически, аналогично определению адреса перехода в структурах управления.

Можно пойти еще дальше и вместо прямой нумерации локальных объектов ввести их настоящее описание через соответствующее слово внутри определения через двоеточие с автоматическим переносом входных параметров на локальный стек при входе в такое определение. Словарная статья для имени локального объекта, которая строится по такому описанию, должна располагаться несколько выше текущей вершины словаря и иметь признак немедленного исполнения. Ее действие состоит в компиляции обращения к соответствующему элементу локального стека. По завершении компиляции данного определения все такие временные статьи автоматически исключаются из словаря.

Введение локальных переменных не только упрощает программирование, освобождая программиста от необходимости тщательно отслеживать все изменения на стеке, но и сокращает размер скомпилированной программы, поскольку из нее исчезают сложные последовательности из слов OVER, DUP, ROT, PICK и других, обычно используемых для доступа к локальным значениям. Такое неожиданное сочетание приятного с полезным - одна из многих удивительных сторон языка Форт, которые открываются программисту при знакомстве с ним.


3

3.4. Векторное поле кода

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

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

Именно в этом, например, заключается действие стандартного слова LITERAL, которое анализирует текущее состояние текстового интерпретатора через значение переменной STATE. Если это значение - нуль (состояние исполнения), то это слово больше ничего не делает. При ненулевом значении (состояние компиляции) оно снимает значение с вершины стека и компилирует его как литерал на вершину словаря.

Пример другого подхода дает описанная в п. 3.2 реализация оператора присваивания в традиционной инфиксной форме записи. В зависимости от текущего значения переменной ?ЗНАЧ, которое изменяется словами := и ;, слова, обозначающие переменные, оставляют на стеке либо адрес, либо значение данной переменной.

Предлагаемое улучшение связано прежде всего с использованием переменных. В стандартном определении при исполнении переменной на стеке будет оставлен адрес ячейки (поля параметров), в которой хранится ее значение. Однако сам по себе этот адрес редко бывает нужен, обычно он используется либо для получения текущего значения переменной с помощью слова @, либо для засылки в нее нового значения словом !. Поэтому можно считать, что с переменной связано не одно действие, а три - получение текущего значения, засылка нового и получение адреса. Какое именно действие требуется в каждом конкретном случае, определяется контекстом.

Введем слово QUAN (от QUANTITY - величина), которое определяет новое слово с тремя описанными выше действиями. В словарной статье таких слов вместо одного поля кода создается три - по одному на каждое действие. Будем обозначать их адреса через 0CFA, 1CFA и 2CFA соответственно (рис. 3.1). За ними располагается ячейка, отведенная под текущее значение данной переменной, обозначим ее адрес через 0PFA Если рассматривать такую структуру как обычную словарную статью, то поле 0CFA является полем кода, а поля 1CFA, 2CFA и 0PFA занимают поле параметров. Если в шитый код скомпилирован адрес 0CFA, то при исполнении соответствующего кода в качестве адреса поля параметров выступает адрес 1CFA. Аналогично для адреса 1CFA полем параметров служит 2CFA, а для 2CFA - адрес 0PFA. Поэтому описанные выше три действия можно задать так:

    : ЗНАЧ  DOES> ( 1CFA->N:ЗНАЧ) 4 + ! ;

    : ПРИСВ DOES> ( N:ЗНАЧ,2CFA->) 2+ ' ;

    : АДР   DOES> ( 0PFA->0PFA )   ;

QUAN X /│\ VARIABLE X /│\

┌────────┬───────┐ │ ┌────────┬───────┐ │

NFA│ 1 │ "X" │ │ │ 1 │ "Y" │ │

├────────┴───────┤ │ ├────────┴───────┤ │

LFA│ ■───────│────┘ │ ■───────│────┘

├────────────────┤ ├────────────────┤

0CFA│--> значение │ CFA│--> адрес │

├────────────────┤ ├────────────────┤

1CFA│--> присваивание│ PFA│текущее значение│

├────────────────┤ └────────────────┘

2CFA│--> адрес │

├────────────────┤

0PFA│текущее значение│

└────────────────┘

Рис. 3.1. Структура статьи с векторным полем кода

Нетрудно увидеть, что действие АДР совпадает со стандартным действием для переменной, состоящим в том, что адрес поля параметров кладется на стек. Определим теперь слово QUAN, используя слово ПРИСВ в качестве вспомогательного:

    : QUAN ( ->) CREATE LATEST NAME> DUP @ ( КОД 2CFA)

        ПРИСВ SWAP @ ( КОД 2CFA,КОД 1CFA) , , 0 ,

        DOES> ( КОД ДЛЯ 0CFA) 4 + @ ;

Создающая часть этого определения использует поле кода создаваемой статьи как рабочую ячейку, из которой сначала извлекается значение для 2CFA, засланное туда словом CREATE, и затем значение для 1CFA, которое засылается туда словом ПРИСВ. Окончательное значение в этой ячейке устанавливается словом DOES>. Описание переменной через слово QUAN выглядит так же, как описание обычной переменной: QUAN X. Однако теперь при исполнении слова Х на стеке будет оставлено текущее значение переменной из ее поля 0PFA. Выполнение двух других действий задают слова ТО (предлог "в") и AT (предлог "из"):

    : TO  ' 2+ STATE @ IF , ELSE EXECUTE THEN ; IMMEDIATE

    : AT  ' 4 + STATE @ IF , ELSE EXECUTE THEN ; IMMEDIATE

Эти слова имеют признак немедленного исполнения и в состоянии компиляции (внутри определения через двоеточие) компилируют коды 1CFA и 2CFA для следующего слова. В состоянии исполнения соответствующие действия исполняются. Теперь чтобы присвоить переменной X значение 100, нужно выполнить текст 100 TO X, а для получения адреса - текст AT X.

Такое усовершенствование, как и введение локальных переменных, не только упрощает программирование, но и сокращает объем скомпилированного кода. Из текста программ исчезает слово @, применявшееся для разыменования адреса переменной. В результате вместо двух ссылок - на поле CFA для переменной и статью для @ - в шитом коде присутствует только одна ссылка (на поле 0CFA). Аналогично в случае присваивания вместо двух ссылок (на поле CFA переменной и на статью для !) компилируется одна (на поле 1CFA). В практических реализациях программы для действий ЗНАЧ и ПРИСВ обычно задаются не в виде высокоуровневых определений, а непосредственно в машинном коде данной ЭВМ через встроенный ассемблер форт-системы. В этом случае описание переменных через слово QUAN не только сокращает размер программы, но и повышает ее быстродействие, поскольку отпадает необходимость в исполнении действия NEXT для интерпретации еще одной ссылки.

По аналогии со словом QUAN определим слово VECT (от VECTOR - вектор), которое также создает словарную статью с векторным полем кода из трех элементов:

    : VECT ( ->) 0 CONSTANT

        LATEST NAME> DUP @ , ( КОД 2CFA)

        ПРИСВ DUP @ (  1CFA) SWAP >BODY !  ['] ABORT ,

        DOES> ( КОД ДЛЯ 0CFA)  4 + @ EXECUTE  ;

Код для поля 2CFA указывает на исполняющую часть из определяющего слова CONSTANT, поле 1CFA такое же, как и для слова QUAN, оно выполняет присваивание нового значения. Наконец, поле 0CFA, которое задается исполняющей частью определения VECT, исполняет слово, адрес поля кода которого является текущим значением поля 0PFA. Для получения текущего значения и засылки нового по-прежнему используются определенные ранее слова AT и TO. Вот пример на использование этих слов: VECT V / DUP TO V. Теперь исполнение слова V равносильно исполнению DUP. Таким образом, слова, определенные через VECT, можно рассматривать как переменные, значениями которых являются другие слова. Для исполнения слова - текущего значения такой переменной - достаточно указать только имя этой переменной без дополнительных операций @ и EXECUTE. Текст AT V> NAME ID. распечатывает имя слова - текущего значения V. Разумеется, при такой распечатке предполагается, что само это слово имеет обычную структуру словарной статьи.


3

3.5. Выбор по целому

Выбор по целому - распространенная конструкция в языках программирования. Она является обобщением условного оператора, который осуществляет выбор между двумя последовательностями операторов - частью "то" и частью "иначе" - по логическому значению (ИСТИНА или ЛОЖЬ) условия. В конструкции выбора по целому в качестве значения условия выступает целое число, и выбор осуществляется между несколькими альтернативными ветвями, каждая из которых соответствует определенному значению условия или некоторому множеству таких значений. Как правило, множества значений условия для разных ветвей не должны пересекаться. Обычно эти множества задают явным перечислением отдельных значений или указанием диапазона для них. Разберем два варианта реализации выбора по целому. В первом используется переключатель, во втором - вложенные условные операторы.

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

    : SWITCH ( ->) ?EXEC CREATE SMUDGE ]

        DOES> ( I:НОМЕР ВЕТВИ,PFA->) SWAP

        1- 2 * + @ EXECUTE ;

Создающая часть строит новую словарную статью и переключает текстовый интерпретатор в состояние компиляции. Таким образом, следующие слова будут компилироваться в поле параметров определяемого слова. Слово SMUDGE выставляет разряд "Не готов" в байте-счетчике поля имени, делая данную статью "невидимой" при поиске слов в словаре. Этот разряд будет сброшен словом ; (точка с запятой), которое завершает компиляцию такого переключателя. Например:

    : W1 ." ПОНЕДЕЛЬНИК" ;    : W2 ." ВТОРНИК" ;

    : WЗ ." СРЕДА" ;          : W4 ." ЧЕТВЕРГ" ;

    : W5 ." ПЯТНИЦА" ;        : W6 ." СУББОТА" ;

    : W7 ." ВОСКРЕСЕНЬЕ" ;

    SWITCH ДЕНЬ-НЕДЕЛИ  W1 W2 WЗ W4 W5 W6 W7 ;

Порядок использования переключателя ДЕНЬ-НЕДЕЛИ иллюстрирует следующий протокол работы:

    > 3  ДЕНЬ-НЕДЕЛИ

    СРЕДА OK

    > 5  ДЕНЬ-НЕДЕЛИ

    ПЯТНИЦА  OK

Описанный механизм аналогичен определению вектора, рассмотренному в п. 1.10. Его также можно задавать по-разному в зависимости от конкретных требований. В приведенной реализации неправильное значение выбирающего условия (выходящее за диапазон от 1 до 7) приведет к непредсказуемому результату, поскольку соответствующий контроль отсутствует. Чтобы ввести его в реализацию, нужно каким-то образом подсчитать число скомпилированных ссылок при завершении компиляции. Для этого можно применить способ, аналогичный компиляции переходов в шитом коде, введя вместо точки с запятой специальное слово, отмечающее конец переключателя.

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

          : CASE ( ->A0:CSP,4)  ?COMP CSP @ !CSP 4 ;

                                               IMMEDIATE

          : OF ( 4->A:>MARK,1,5) 4 ?PAIRS COMPILE OVER

            COMPILE = [COMPILE] IF  COMPILE DROP 5 ;

                                               IMMEDIATE

          : ENDOF ( A:>>MARK,1,5->A2:>MARK,1,4)   5 ?PAIRS

                [COMPILE] ELSE  4 ;            IMMEDIATE

          : ENDCASE ( A0,A1,1, ..,AN,4->)

            4 ?PAIRS COMPILE DROP

            BEGIN SP@  CSP @ = 0= WHILE  [COMPILE] THEN

            REPEAT ( А0) CSP ! ;               IMMEDIATE

          : ДЕНЬ-НЕДЕЛИ ( N->)  CASE

          1 OF ." ПОНЕДЕЛЬНИК" ENDOF  2 OF ." ВТОРНИК" ENOOF

          3 OF ." СРЕДА" ENDOF  4 OF ." ЧЕТВЕРГ" ENDOF

          5 OF ." ПЯТНИЦА" ENDOF  6 OF ." СУББОТА" ENDOF

          7 OF ." ВОСКРЕСЕНЬЕ" ENDOF

            CR . ." - ДЕНЬ НЕДЕЛИ?" ABORT ENDCASE ;

Слова CASE (выбор) и ENDCASE (конец выбора) ограничивают конструкцию и обеспечивают правильную компиляцию вложенных операторов. Слово CASE, проверив, что текстовый интерпретатор находится в состоянии компиляции, сменяет глобальную переменную CSP (сокращение от CURRENT STACK POINTER - текущий указатель стека), сохраняя на стеке ее прежнее значение (слово !CSP засылает в переменную CSP адрес текущей вершины стекав. Слова OF (из) и ENDOF (конец из) ограничивают отдельную ветвь. Во время работы скомпилированного определения перед началом исполнения каждой ветви на стеке лежат два значения: число, представляющее условие выбора, и номер данной ветви. Слово OF компилирует текст OVER = IF DROP, который обеспечивает передачу управления на данную ветвь, если эти два значения совпали, причем в этом случае они оба снимаются со стека. Если же значения оказались разными, то управление передается на текст, следующий за словом ENDOF для данной ветви, которое эквивалентно слову ELSE. Наконец, слово ENDCASE компилирует операцию DROP, чтобы снять со стека оставшееся там значение условия, и разрешает все накопившиеся на стеке выходы из ветвей на текущую точку, исполняя для этих ветвей слово THEN. Его последним действием является восстановление прежнего значения переменной CSP, которое к этому моменту оказывается на вершине стека.

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

    : (OF) ( N:УСЛОВИЕ,I:НОМЕР ВЕТВИ->)

        OVER = IF DROP R> 2+ ELSE R> @ THEN  >R ;

    : OF ( 4->A:>MARK,2,5)  4 ?PAIRS

        COMPILE (OF) >MARK  2  5 ;     IMMEDIATE

Исполнение слова (OF) аналогично ?BRANCH, в зависимости от условия оно переустанавливает указатель интерпретации, либо обходя скомпилированную за ним ссылку, либо устанавливая указатель по значению этой ссылки. Аналогичным образом наряду со словами (OF) и ОФ можно определить пары (<OF) и <OF, (>OF) и >OF , (<OF<) и <OF< , выполняющие сравнение значения условия с заданным значением, выбирающим данную ветвь, не на равенство, а на неравенство указанного вида. При этом слово (<OF<) сравнивает значение условия с двумя значениями, определяющими интервал. Например:

    : ПРИЕМ ( N:НОМЕР ДНЯ->)   CASE

        3 OF ." НЕПРИЕМНЫЙ" ENDOF

        1 5 <OF< ." ПРИЕМНЫЙ" ENDOF

        6 7 <OF< ." ВЫХОДНОЙ" ENDOF

        CR . ." - НОМЕР ДНЯ?" ABORT ENDCASE  ." ДЕНЬ" ;

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


3

3.6. Динамическая идентификация

Внутренним представлением форт-слова является словарная статья, которая размещается в словаре. Через поля связи словарные статьи объединяются в цепные списки, каждый из которых также представлен как отдельный объект словарной статьей, создаваемой по слову VOCABULARY. Исполнение слова, обозначающего такой список, делает его текущим значением переменной CONTEXT, определяющей список, в котором начинается поиск каждого вводимого форт-слова. Если слово отсутствует в этом списке, то следующим просматривается список, в который добавляются создаваемые новые статьи, текущее значение переменной CURRENT. Последним просматривается стандартный список FORTH. Если текстовый интерпретатор находится в состоянии исполнения, то найденный код исполняется, в состоянии компиляции он компилируется в виде ссылки на данную словарную статью для последующего исполнения в составе скомпилированного определения.

Таким образом, в процессе создания нового определения мы индентифицируем составляющие его слова статически, т.е. по текущему контексту (значениям переменных CONTEXT и CURRENT) во время компиляции. Во время исполнения этого определения будут исполняться именно эти, найденные во время компиляции составляющие слова. Вместе с тем в практике программирования уже давно применяется прием, известный как динамическая идентификация. В применении к языку Форт он состоит в том, что вместо компиляции ссылки на статью слова по результату статической индентификации в компилируемой статье запоминается имя слова, и его поиск ведется в момент фактического исполнения. Главная особенность состоит в том, что контекст для поиска также устанавливается динамически в результате предшествующих вычислений.

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

В качестве примера рассмотрим простой язык, аналогичный рассмотренному в п.3.2, и содержащий описания констант, переменных и оператор присваивания с инфиксной формой записи для выражений. Пусть описания констант выглядят, например, так: КОНСТ А = 2. Чтобы не перегружать наш пример лишними деталями, не будем учитывать возможность компиляции описаний и операторов этого языка. Пусть они сразу же исполняются по мере поступления. Собственно задача состоит в том, что при обработке левой части оператора присваивания можно было различать переменные и константы. В первом случае результатом исполнения должен быть адрес соответствующей ячейки памяти, а во втором - сообщение об ошибке - недопустимом использовании константы в качестве получателя присваивания. Если же имя переменной или константы использовано в правой части присваивания, то в обоих случаях нужно получить соответствующее значение. Разумеется, соответствующие операции проверки можно включить в программу для переменной и константы, анализируя глобальную переменную ?ЗНАЧ, как это сделано в рассмотренном выше примере. Покажем, как эту же задачу можно решить через динамическую идентификацию.

Определим два контекста (списка слов) П и К, содержащих операции над переменными и константами. Таких операций две: ЗНАЧ для получения значения и АДР для получения адреса. Операция ЗНАЧ работает в обоих случаях одинаково, а операция АДР допустима только для переменной; будучи примененной к константе, она должна выдать сообщение об ошибке. В список FORTH также включим слово ЗНАЧ, которое в этом контексте выполняет пустую операцию, и слово АДР, выдающее сообщение об ошибке (их назначение станет ясным чуть позже). Для определения всех этих слов примем, что значение переменной или константы размещается в поле параметров созданной для нее словарной статьи.

    FORTH DEFINITIONS

    VOCABULARY П   VOCABULARY К

    П DEFINITIONS ( ДЕЙСТВИЯ С ПЕРЕМЕННЫМИ)

    : ЗНАЧ ( PFA->N:ЗНАЧ)  @ FORTH ;

    : АДР ( PFA->A:АДР)  FORTH ;

    К DEFINITIONS ( ДЕЙСТВИЯ С КОНСТАНТАМИ)

    : ЗНАЧ ( PFA->N:ЗНАЧ) @ FORTH ;

    : АДР  ( PFA->) CR

        ." НЕДОПУСТИМОЕ ИСПОЛЬЗОВАНИЕ КОНСТАНТЫ "

        BODY> >NAME ID. ABORT ;

    FORTH DEFINITIONS

    : ЗНАЧ ( ->) ;

    : АДР ( ->) -1 ABORT" НЕДОПУСТИМЫЙ АДРЕС" ;

Заметим, что при исполнении слов ЗНАЧ и АДР в контекстах П и К помимо вычисления соответствующего значения текущий контекст переключается на FORTH. Исполняющая часть определяющих слов ПЕРЕМ и КОНСТ теперь состоит в установке соответствующего контекста, при этом на стек кладется адрес поля параметров данной статьи:

    : ПЕРЕМ ( ->) CREATE 0 , DOES> ( PFA->PFA) П ;

    : КОНСТ ( ->) CREATE BL WORD DROP

        ( ВЫБРАТЬ ЗНАК =)

        BL WORD NUMBER DROP ( N:ЗНАЧ) ,

        DOES> ( PFA->PFA) К ;

Чтобы теперь при обработке выражения в левой части присваивания получать значение переменной или константы, надо слегка изменить определение слова >ОПРЦ> (см. п.3.2) - включить в него исполнение операции ЗНАЧ, которая идентифицируется динамически по текущему контексту.

    : >ОПРЦ> ( N:ПРИОРИТЕТ->)

        >R " ЗНАЧ" FIND DROP EXECUTE

        BEGIN ОПРЦ@ R@ < NOT WHILE   ОПРЦ>  DROP

        ОПРЦ> ( CFA) EXECUTE REPEAT R> DROP ;

Если это определение исполняется сразу после переменной или константы (в контексте П или К), то оно преобразует адрес поля параметров соответствующей статьи, который находится в этот момент на вершине стека, в значение данвоа переменной или константы. Для всех других случаев (контекст FORTH) преобразования стека не происходит. Именно по этой причине определения ЗНАЧ в списках П и К по завершении их исполнения переключают текущий контекст на FORTH, а в списке FORTH присутствует определение ЗНАЧ с пустым действием. Последнее обстоятельство гарантирует успешный поиск слова ЗНАЧ в любом контексте, поэтому в приведенном определении проверка того, что поиск закончился успешно, опущена (is DROP-ped). В заключение осталось только аналогичным образом переопределить слово := (см. п.3.2):

    : :=  " АДР" FIND DROP EXECUTE  [COMPILE] ( ;

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

    К DEFINITIONS

    : ЗНАЧ ( PFA->N:ЗНАЧ)
[ П ]
ЗНАЧ ;

В приведенном определении слово ЗНАЧ для списка К не программируется вновь, а определяется через такое же слово из контекста П, тем самым сокращается общий объем отладки. Следуя по этому пути, можно вообще исключить слово АДР из списка К, поскольку в этом случае сообщение об ошибке выдаст слово АДР из списка FORTH, правда, с меньшей информацией - не будет напечатано имя константы. Если механизм динамической идентификации применяется для ряда не связанных между собой целей, то может оказаться неудобным выполнять переключение контекстов через одну и ту же глобальную переменную CONTEXT и выполнять поиск слова в текущем контексте словом FIND. Как правило, все форт-системы имеют более элементарное слово (FIND), которое получает в качестве параметра текстовую строку с именем слова и вход в список статей, в котором нужно выполнять поиск. Используя это слово, можно организовать поиск слова в любом списке или группе списков, уже не связывая его с текущим порядком поиска, принятым для слова FIND.

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


3

3.8. Сопрограммы

Представление задачи в виде взаимодействующих сопрограмм (процессов) в теории программирования стало уже традиционным [15,16]. Но на практике этот метод используется редко, так как большинство распространенных языков высокого уровня не имеют необходимых конструкций. В стандарт языка Форт также не включены средства для сопрограммной работы. Вызвано это в основном тем, что такие средства обычно зависят от конкретного приложения и могут варьироваться очень сильно. Тем не менее, используя только стандартные для большинства систем слова, можно реализовать необходимые конструкции.

Рассмотрим достаточно простую и экономную реализацию сопрограмм. Направления, в которых можно ее усовершенствовать, рассмотрены ниже.

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

Будем считать, что адрес области памяти текущей сопрограммы находится в переменной Т-С. Используя ее, введем слова для доступа к полям этой области. Для простоты в данной реализации отводится участок памяти размером 128 байт. В его начале находятся: Т-ПРОГ - указатель начала шитого кода, соответствующего исполняемой программе, Т-СТЕК - указатель вершины стека данных на момент последней приостановки данной сопрограммы (указатель вершины стека возвратов сохраняется на стеке данных) и Т-ВЫЗВ - адрес аналогичной области для сопрограммы, вызвавшей данную. В остальной памяти участка размещаются стек данных и стек возвратов по 60 байт каждый.

Внешняя программа, в рамках которой действует описываемый механизм сопрограмм, представлена 6-байтной областью ВНЕШ, в которой размещаются перечисленные выше значения. В качестве своих стеков эта сопрограмма использует исходные стеки форт-системы.

Перед началом работы все сопрограммы должны быть проинициализированы словом START (старт), завершение работы сопрограммного механизма вызывает слово STOP (стоп).

Сопрограмма определяется через слово СОПРОГРАММА, которое по своему употреблению аналогично двоеточию. Вслед за ним идет имя сопрограммы и форт-текст, задающий требуемую программу. Обычно она состоит из цикла, внутри которого используется слово RESUME (возобновить) для приостановки данной сопрограммы и возобновления сопрограммы, ее вызвавшей. Выход по EXIT или ; приводит к завершению работы всего сопрограммного механизма и возобновлению текстовой интерпретации.

    QUAN Т-С   ( АДРЕС ОБЛАСТИ ТЕКУЩЕЙ СОПРОГРАММЫ)

    : Т-ПРОГ ( ->A:АДРЕС НАЧАЛА ПРОГРАММЫ)  Т-С ;

    : Т-СТЕК ( ->A:АДРЕС УКАЗАТЕЛИ СТЕКИ )  Т-С 2+ ;

    : Т-ВЫЗВ ( ->A:АДРЕС ВЫЗВАВШЕЙ СОПРОГРАММЫ) Т-С 4 + ;

    CREATE ВНЕШ 6 ALLOT ( ОБЛАСТЬ ДЛЯ ВНЕШНЕЙ ПРОГРАММЫ)

    : RESUME ( -> ОСТАНОВИТЬ ТЕКУЩ.И ВОЗОБН. ВЫЗВАВШУЮ)

        RP@  SP@  Т-СТЕК ! ( СОХРАНИТЬ СОСТОЯНИЕ ТЕКаВЕИ)

        Т-ВЫЗВ @  TO Т-С  ( ПЕРЕКЛЮЧИТЬСЯ НА ВЫЗВАВШУЮ ДАННУЮ)

        Т-СТЕК @  SP!  RP!  ; ( ВОЗОБНОВИТЬ ПРИОСТАНОВЛЕННУЮ)

    : STOP ( -> ЗАВЕРШЕНИЕ ВСЕХ СОПРОГРАММ) QUIT ;

    : (START) ( A:АДРЕС ОБЛАСТИ СОПРОГРАММЫ-> ИНИЦИАЛИЗ)

        DUP TO Т-С  Т-ПРОГ @ OVER 126 + !  DUP 126 +

        OVER 66 + !  66 + Т-СТЕК !  ВНЕШ TO Т-С  ;

    : СОПРОГРАММА ( -> ОПРЕДЕЛЕНИЕ СОПРОГРАММЫ) CREATE HERE

        DUP 132 + , 126 ALLOT HERE 2+ , ['] STOP , (START) ]

        DOES> ( PFA:ОБЛАСТЬ СОПРОГРАММЫ->) RP@* SWAP >R

        SP> Т-СТЕК !  Т-С R> TO Т-С Т-ВЫЗВ !

        Т-СТЕК @ SP! RP!  ;

    : START ( -> ИНИЦИАЛИЗИРОВАТЬ СОПРОГРАММУ)

        ' >BODY [COMPILE] LITERAL   STATE @

        IF COMPILE (START) ELSE (START) THEN ;  IMMEDIATE

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

Применяя описанный механизм, определим две сопрограммы ВВОД и ВЫВОД, которые обмениваются между собой через однобайтный буфер ТЕК-СИМ, и слово ЗАДАЧА, которое выполняет требуемую перепись данных:

    QUAN ТЕК-СИМ   ( ОЧЕРЕДНОЙ ОБМЕНИВАЕМЫЙ СИМВОЛ)

    CREATE ВХОД-БУФ 80 ALLOT  ( БУФЕР ВВОДА)

    CREATE ВЫХ-БУФ  64 ALLOT  ( БУФЕР ВЫВОДА)

    СОПРОГРАММА ВВОД

        ОТКРЫТЬ-ВВОД  BEGIN  ВХОД-БУФ ЧИТАТЬ WHILE

        ВХОД-БУФ 80 + ВХОД-БУФ DO  I С@ TO ТЕК-СИМ

        RESUME LOOP

        REPEAT  ЗАКРЫТЬ-ВВОД  ЗАКРЫТЬ-ВЫВОД ;

    СОПРОГРАММА ВЫВОД

        ОТКРЫТЬ-ВЫВОД BEGIN  ВЫХ-БУФ 64 + ВЫХ-БУФ DO

        ТЕК-СИМ I С!  RESUME  LOOP

        ВЫХ-БУФ ПИСАТЬ  AGAIN ;

    : ЗАДАЧА

        START ВВОД  START ВЫВОД

        BEGIN ВВОД ВЫВОД AGAIN ;

Два ряда слов ОТКРЫТЬ-ВВОД, ЧИТАТЬ, ЗАКРЫТЬ-ВВОД и ОТКРЫТЬ-ВЫВОД, ПИСАТЬ, ЗАКРЫТЬ-ВЫВОД обеспечивают взаимодействие с входным и выходным файлами. Слова ЧИТАТЬ и ПИСАТЬ требуют в качестве параметра адрес буфера (ВХОД-БУФ для ввода и ВЫХ-БУФ для вывода), а слово ЧИТАТЬ , кроме того, возвращает логический результат - признак успешного завершения чтения.

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

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

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

3. Предусмотрев поле связи для сцепления всех сопрограмм в список, можно получить состояние всей совокупности взаимодействующих сопрограмм.

4. Можно предусмотреть вызов асинхронного выхода "Окончание задачи". Идентификация этого выхода может быть как статической, так и динамической.

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

6. Если использовать косвенный вызов через переменную типа VECT, можно динамически определять возобновляемую сопрограмму.

Возможны, конечно, и любые другие изменения, диктуемые конкретной обстановкой.


3

3.9. Запланированное перекрытие

Адресное пространство форт-системы не так уж велико - 64 К байт. Это налагает известное ограничение на общее число форт-слов, одновременно присутствующих в словаре. Для снижения этого ограничения можно применить свернутый шитый код или использовать форт-систему, в которой в качестве основного вместо 16-разрядного значения выступает 32-разрядное. Можно заметно сократить требуемый объем оперативной памяти, если использовать векторное поле кода.

Опишем еще один простой способ снижения требований к памяти - запланированное перекрытие. Группа взаимосвязанных определений образует единый сегмент в двоичном коде, который может быть загружен на заранее заданное место в словаре из внешней памяти форт-системы. Разные сегменты, загружаемые на одно и то же место оперативной памяти, перекрывают друг друга, вот почему такая структура и названа запланированным перекрытием. Благодаря тому, что хранящийся во внешней памяти сегмент выражен в двоичном коде, а не в виде текста, его загрузка идет примерно в 50-100 раз быстрее, чем обычная текстовая интерпретация.

В приводимой далее реализации каждый сегмент загружается на фиксированный адрес - значение указателя HERE на момент начала его компиляции Определение сегмента включается в список FORTH. После того как трансляция сегмента завершена, при его выгрузке во внешнюю память указатель вершины словаря понижается до прежнего значения с одновременным исключением всех слов данного сегмента из списка FORTH. При загрузке сегмента на соответствующий адрес указатель вершины словаря вновь поднимается, и определения сегмента снова включаются в список FORTH.

Для трансляции сегментов введем глобальную переменную (СЕГМ), содержащую адрес начала очередного компилируемого сегмента, границы которого отмечаются словами СЕГМ-НАЧ и СЕГМ-КОН. Выгрузку и загрузку сегментов выполняют слова СЕГМ-ВЫГР и СЕГМ-ЗАГР.

    VARIABLE (СЕГМ)

    : СЕГМ-НАЧ  ( -> НАЧАЛО ТРАНСЛЯЦИИ СЕГМЕНТА)

        HERE DUP (СЕГМ) ! 0 ,     ( НОМЕР ЭКРАНА)

        HERE  0 C,  BL C,       ( ФИКТИВНОЕ ИМЯ)

        FORTH DEFINITIONS LATEST , ( ЗВЕНО СВЯЗИ)

        CURRENT @ !     ,    ( ТОЧКА ЗАГРУЗКИ)

        0 , 0 ,    ( ДЛИНА И ПОСЛЕДНЯЯ СТАТЬЯ)

        ;

    : СЕГМ-КОН   ( -> КОНЕЦ ТРАНСЛЯЦИИ СЕГМЕНТА)

        (СЕГМ) @ >R    ( ТОЧКА ЗАГРУЗКИ СЕГМЕНТА)

        HERE R@ -    ДЛИНА) DUP  R@ 8 + !

        FORTH DEFINITIONS  LATEST R> 10 + ! ;

    : СЕГМ-ВЫГР ( N:НОМЕР ЭКРАНА->  ВЫГРУЗКА)

        DUP (СЕГМ) @ >R R@ ! ( НОМЕР)

        R@ SWAP ( A:АДРЕС СЕГМЕНТА,N) DUP R@ 8 + @

        1023 + 1024 / ( A,N,N,N1:ЧИСЛО ЭКРАНОВ)

        CR ." СЕГМЕНТ ВИГРУЖЕН НА ЭКРАНЫ: " + SWAP DO

        I . ( AI:АДРЕС СЛЕДУЮЩЕИ ПОРЦИИ) DUP I BUFFER

        1024 CMOVE UPDATE 1024 +LOOP DROP

        FLUSH R> (FORGET) ;

    : СЕГМ-ЗАГР ( N:НОМЕР ЭКРАНА-> ЗАГРУЗКА)

        DUP BLOCK ( N,A0:АДРЕС ПЕРВОГО БЛОКА СЕГНЕНТА)

        2DUP @ - IF DROP CR . ." - БЛОК ДЛЯ ЗАГРУЗКИ?"

        ABORT THEN    DUP 6 + 1 (FORGET) 2DUP

        HERE ( N,A0,N,A0,A1:НАЧАЛО) SWAP 8 + @ ( N,A0,N,A1,L)

        DUP ALLOT 1023 + 1024 / ( N,A0,N,A1,N1:ЧИСЛО ЭКР) ROT

        DUP ROT + SWAP ( ...N1+N,N) DO I BLOCK OVER 1024 CMOVE

        1024 +  LOOP 2DROP ( N) BLOCK 10 + @  CURRENT @ !  ;

Слово СЕГМ-НАЧ строит заголовок сегмента, который располагается в его начале и содержит начальный номер экрана во внешней памяти, фиктивную словарную статью, адрес начала сегмента в оперативной памяти, длину сегмента и адрес начала его последней словарной статьи. Номер экрана определяется программистом при выгрузке сегмента во внешнюю память, впоследствии именно с этого экрана начинается его загрузка. Слово СЕГМ-ЗАГР проверяет, содержат ли первые 2 байта сегмента данный номер, и выдает сообщение об ошибке, если это не так.

Фиктивная словарная статья в заголовке сегмента аналогична полю параметров для слов, определенных через VOCABULARY , и служит для включения определений данного сегмента в список FORTH при загрузке сегмента. Она сцепляется с последней статьей списка FORTH на момент начала трансляции сегмента. При загрузке сегмента список FORTH устанавливается на последнее определение внутри данного сегмента. Таким образом, словарные статьи сегмента вновь включаются в словарь форт-системы. При изменениях состояний словаря, связанных с загрузкой и выгрузкой сегментов, в качестве вспомогательного используется слово (FORGET), которое корректным образом понижает вершину словаря, исключая из него все системные ссылки на исключаемые статьи. В качестве параметра это слово использует новый адрес вершины словаря (см. модель форт-системы в приложении 1).


3

3.10. Элементарная машинная графика

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

Вместе с тем развитие машинной графики в значительной степени определяется наличием тех или иных аппаратных средств. Чрезвычайное разнообразие их конкретных характеристик делает невозможным создание какого-либо универсального языка, применимого для всех возможных случаев. Язык Форт позволяет программисту самому быстро создавать необходимые инструментальные средства, применяя универсальные приемы с учетом особенностей конкретной задачи.

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

В случае использования алфавитно-цифрового терминала задача состоит в том, чтобы отобразить в нем график функции, заданной вектором своих значений. Введем понятие вектора, элементы которого нумеруются от нуля, по аналогии со словом QUAN (см. п.3.4). Для получения адреса элемента вектора и для присваивания ему нового значения можно использовать те же слова AT и TO.

    : ?+ ( N->N) DUP 0< ABORT" ОТРИЦАТЕЛЬНОЕ ЗНАЧЕНИЕ" ;

    : В-АДР  ( I:ИНДЕКС,PFA->A[I]:АДРЕС)  2DUP @ U<

        IF SWAP 1+ 2* + EXIT THEN   SWAP CR

        .  ."  - НЕДОПУСТИМЫЙ ИНДЕКС ДЛЯ ВЕКТОРА "

        BODY> BODY> BODY>  >NAME  ID.  ABORT ;

    : В-АДР0  DOES> ( I:ИНДЕКС,PFA->A[I]:АДРЕС)  В-АДР ;

    : В-ПРИСВ DOES> ( N:3НАЧ,I:ИНДЕКС,2CFA->) 2+ В-АДР ! ;

    : VQUAN  ( N:ВЕРХН.ИНДЕКС->) ?+ CREATE В-ПРИСВ HERE 2- @ ,

        В-АДР0 HERE 4 - @ ,  1+ DUP ,  2* HERE SWAP DUP ALLOT

        ERASE  DOES> ( I:ИНДЕКС,1CFA->N[I]:ЗНАЧ)  4 + В-АДР @ ;

    : В-РАЗМ  ( ->N:ЧИСЛО ЭЛЕМЕНТОВ)

        ' 6 + @  [COMPILE] LITERAL ;  IMMEDIATE

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

        ( 18.06.86 SIN COS)    ( ТАБЛИЦА СИНУСОВ ОТ 0 ДО 90 ГРАД.) HERE

        00000 , 0175 , 0349 , 0523 , 0698 , 0872 , 1045 , 1219 , 1392 ,

         1564 , 1736 , 1908 , 2079 , 2250 , 2419 , 2588 , 2756 , 2924 ,

         3090 , 3256 , 3420 , 3584 , 3746 , 3907 , 4067 , 4226 , 4384 ,

         4540 , 4695 , 4848 , 5000 , 5150 , 5249 , 5446 , 5592 , 5736 ,

         5878 , 6018 , 6157 , 6293 , 6428 , 6561 , 6691 , 6820 , 6947 ,

         7071 , 7193 , 7314 , 7431 , 7547 , 7660 , 7771 , 7880 , 7986 ,

         8090 , 8192 , 8290 , 8387 , 8480 , 8572 , 8860 , 8746 , 8829 ,

         8910 , 8988 , 9063 , 9135 , 9205 , 9772 , 9336 , 9397 , 9455 ,

         9511 , 9563 , 9613 , 9659 , 9703 , 9744 , 9781 , 9816 , 9840 ,

         9877 , 9903 , 9925 , 9945 , 9962 , 9976 , 9986 , 9994 , 9998 ,

        10000      : SIN180 ( N->SIN N, 0<=N<=180)

        DUP 90 > IF 180 SWAP - THEN 2* [ DUP ] LITERAL + @ ;       DROP

        : SIN ( N->SIN N)  360 MOD DUP 0< IF 360 + THEN

              DUP 180 > IF 180 - SIN180 NEGATE ELSE SIN180 THEN ;

        : COS  ( N>-COS N)  90 SWAP - SIN ;

Аналогично и другие элементарные функции можно с успехом вычислять по классическим итерационным схемам:

          (  DSQRT  SQRT  ИЗВЛЕЧЕНИЕ КОРНЯ ПО  СХЕМЕ НЬЮТОНА)

          : DSQRT ( D1->D2)  2DUP

                          D0< ABORT" ОТРИЦАТЕЛЬНЫЙ АРГУМЕНТ"

            2DUP 2. D< IF EXIT THEN

          ( ДАЛЕЕ ПО СХЕМЕ: X[0]=X/2; X[I+1]=X[I]/2+X/X[I]/2)

            2DUP  2. D/ SWAP

            BEGIN 2OVER 2OVER D/ 2OVER D+ 2. D/ 2SWAP 2OVER

                  D- DABS 2. D< UNTIL

            2SWAP 2DROP ;

          : SQRT  ( N1->N2)  S>D  DSQRT  DROP ;

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

Используя описанные вспомогательные средства, можно задавать векторы со значениями интересующих нас функций. Работая с простыми значениями, будем пользоваться их описанием через слово QUAN. Пусть наша функция в обычной записи имеет вид: y = sin Зx + sin 120x. Зададим ее определение через введенные инструментальные определения, выбрав 100 в качестве масштабного множителя:

          70 VQUAN U

          : U!  ( -> УСТАНОВКА ВЕКТОРА U) В-РАЗМ U  0  DO

               3 I * SIN  120 I * SIN + 100 / I TO U LOOP ;

          U!

При желании можно проверить правильность установки значений, распечатав их как числа:

          : B-?  ( ->  РАСПЕЧАТКА ВЕКТОРА)  >IN @ >R

              [COMPILE] В-РАЗМ  R> >IN ! ' SWAP 0 DO

                I OVER EXECUTE  8 .R  LOOP  DROP  ;

В результате будет напечатан следующий текст:

           0      93     -78      15     109     -62      30     124

         -47      45     138     -34      58     151     -21      70

         162     -10      80     172       0       9     179       4

          95      185      9      98     188      11     100     188

          10       9     186       7      95     181       2      89

         177      -4      80     166     -14      70     155     -25

         -67      15      99     -83       0      83     -99     -15

          67    -114     -30      52    -129     -45  OK

Для построения графика определим список слов PLOT (график) и в нем ряд вспомогательных переменных:

          VOCABULARY PLOT   PLOT DEFINITIONS

          QUAN RHO   4 TO RHO    ( ШИРИНА СТОЛБЦА ДЛЯ Y-КООРД.)

          QUAN MU0  16 TO MU0    ( МАКСИМАЛЬНАЯ ВЫСОТА ОКНА)

          QUAN NU0  64 RHO - TO NU0 ( МАКСИМАЛЬНАЯ ШИРИНА ОКНА)

          QUAN MSTA              ( АДРЕС НАЧАЛА ОКНА)

          QUAN MU                ( ТЕКУЩАЯ ВЫСОТА ОКНА)

          QUAN NU                ( ТЕКУЩАЯ ШИРИНА ОКНА)

          QUAN WX                ( X-КООРДИНАТА ОКНА)

          QUAN WY                ( Y-КООРДИНАТА ОКНА)

          QUAN KAPPA             ( ЧИСЛО ЗНАЧЕНИЙ СИГНАЛА)

          QUAN LAMBDA            ( ДИАПАЗОН ЗНАЧЕНИЙ СИГНАЛА)

          QUAN MA                ( МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ СИГН.)

          QUAN MIN0              ( МИНИМАЛЬНОЕ ЗНАЧЕНИЕ В ОКНЕ)

          QUAN VAL               ( ВЕКТОР СИГНАЛА)

Пусть прямоугольный экран терминала, на котором строится график, содержит MU0 строк по NU0 литер каждая. Первые RH0 позиций в каждой строке занимает надпечатка ее координаты по оси ординат. Общий размер графика определяется значениями LAMBDA (диапазон значений функций) и KAPPA (число значений). Этот прямоугольник может как умещаться целиком на экране терминала, так и заметно превышать его. Определим понятие "окна", которое перемещается по графику и показывает на экране терминала соответствующую его часть. Переменные WX и WY задают координаты левого верхнего угла окна относительно левого верхнего угла графика. Определим слова, выполняющие инициализацию и перемещение окна по полю графика:

          FORTH DEFINITIONS PLOT

          : НАЧАТЬ ( -> ИНИЦИАЛИЗАЦИЯ)    PLOT

            HERE NU0 MU0 * ALLOT TO WSTA ;

          PLOT DEFINITIONS

          : ВВЕРХ  ( N->  СДВИНУТЬ ОКНО НА N ПОЗИЦИЙ ВВЕРХ)

             ?+ WY SWAP - 0 MAX TO WY ;

          : ВЛЕВО ( N->  СДВИНУТЬ ОКНО НА N ПОЗИЦИЙ ВЛЕВО)

             ?+ WX SWAP - 0 MAX TO WX ;

          : ВНИЗ  ( N->  СДВИНУТЬ ОКНО НА N ПОЗИЦИЙ ВНИЗ)

             ?* WY + MU + LAMBDA MIN MU - TO WY ;

          : ВПРАВО ( N-> СДВИНУТЬ ОКНО НА N ПОЗИЦИИ ВПРАВО)

             ?+ WX + NU * KAPPA  MIN NU - TO WX ;

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

          : -MU ( N:ЗНАЧЕНИЕ->-1:НИЖЕ ОКНА/

                              M:СМЕЩЕНИЕ ОТ ВЕРХА ОКНА ВНИЗ,0/

                              +1:ВЫШЕ ОКНА )

              MIN0 - DUP 0< IF DROP -1 EXIT THEN

                    MU 1- SWAP - DUP 0< IF DROP 1 EXIT THEN 0 ;

          : ФУНКЦИЯ ( -> СЛЕДУЮЩЕЕ СЛОВО: ИМЯ ВЕКТОРА ЗНАЧЕНИЙ)

             >IN @ >R [COMPILE] В-РАЗМ  R> >IN !

             DUP TO KAPPA NU0 MIN TO NU   ' TO VAL

             ( ВЫЧИСЛЕНИЕ MA И LAMBDA )

             0 VAL DUP   ( X[0],X[0] )

             KAPPA 0 DO I VAL ( MIN[I-1],MAX[I-1],X[I] )

             DUP >R MAX SWAP R> MIN ( MAX[I],MIN[I]) SWAP LOOP

             DUP TO MA  SWAP - 1+ DUP TO LAMBDA  MU0 MIN TO MU

             0 TO WX  0 TO WY ;

          : ОСЬ-X ( НАДПЕЧАТКА ОСИ X) CR RH0 SPACES WX NU 1- RH0 - WX

            BEGIN DUP 10 MOD DUP IF 10 SWAP - THEN DUP >R + 2DUP >=

            WHILE R> SPACES C" ! EMIT DUP 0 <# #S #> DUP >R TYPE

                  1+ R> + REPEAT  RDROP 2DROP ;

          : ГРАФИК ( -> РИСОВАТЬ ГРАФИК ПО СИГНАЛУ И КООРДИНАТАМ ОКНА)

            MA WY - MU 1- - TO MIN0 ( УСТАНОВИТЬ MIN0)

            WSTA MU NU * BLANK ( ЗАЧИСТИТЬ ОКНО)

            0 -MU 0= IF NU * WSTA + NU C" - FILL THEN ( ПРОВЕСТИ ОСЬ X)

            ( ЦИКЛ ПО ОСИ X ДЛЯ ЗАПОЛНЕНИЯ ОКНА)  WX NU + WX DO

              I VAL ( X[I])  -MU DUP IF ( ВНЕ ОКНА) 1+ IF ( ВЫШЕ ОКНА)

                C" + 0 ELSE ( НИЖЕ ОКНА) C" + MU 1- THEN

                                     ELSE DROP C" * SWAP THEN

              NU * I WX - + WSTA + C! LOOP

          ОСЬ-X  MU 0 DO CR  MA WY - I - RH0 .R  I NU * WSTA +

                         NU TYPE LOOP  ОСЬ-X ;

Следующий протокол показывает работу введенных определений по вычерчиванию графика интересующей нас функции:

> НАЧАТЬ ФУНКЦИЯ U ГРАФИК

!0 !10 !20 !30 !40 !50

188 * *

187

186 *

185 *

184

183

182

181 *

180

179 *

178

177 *

176

175

174

173++++++++++++++++++++++ ++ ++ ++ ++ ++ ++ +++++++++++++++++++

!0 !10 !20 !30 !40 !50 OK

> 175 ВНИЗ 10 ВПРАВО ГРАФИК

!10 !20 !30 !40 !50

13+ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ + + +

12

11 *

10 *

9 *

8

7 *

6

5

4 *

3

2 *

1

0----------*---------------------------------------*---------

-1

-2 + + + + + + + + + + ++ ++ ++

!10 !20 !30 !40 !50 OK

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

          ГРАФИК  XY  X  ОТ  0  ДО  4  ШАГ  1

          Y  ЛОГАРИФМ  ОТ  1  ДО  100

          Х  ШТРИХ  РАЗМЕР  0.25  РАМКА

          ЗАГОЛОВОК" ЧЕРТЕЖ 1"  РИСУЙ

В результате будет нарисована прямоугольная рамка с заголовком "ЧЕРТЕЖ 1" надпечаткой оси абсцисс в виде штрихов, пересекающих рамку, и логарифмической разметкой оси ординат. Огромное число других значений, необходимых для построения чертежа, принимается по умолчанию. Далее можно начертить собственно график функции, используя, например, ее задание через вектор значений:

          ФУНКЦИЯ U  МАСШТАБ 100  ЦВЕТ КРАСНЫЙ  РИСУЙ

Аналогично наносим на чертеж дополнительные элементы:

          ПОДЗАГОЛОВОК ВПРАВО

          ТЕКСТ" Y=SIN 3X + SIN 120X"  РИСУЙ

и т.д. Указанные действия можно не только непосредственно исполнять, но и компилировать, задавая впоследствии их исполнение через одно слово – имя скомпилированной программы.


3

3.11. Реализация встроенного ассемблера

Приводимый ниже текст является законченной реализацией встроенного структурного ассемблера для форт-систем на базе микропроцессора К580. В оттранслированном виде он занимает около 1300 байт и является типичным для 8-разрядных микропроцессоров. Для 16- или 32-разрядных процессоров в силу их большей сложности объем текста больше. Но даже в этом случае объем исходного текста и скомпилированного кода существенно меньше, чем для традиционных ассемблеров.

          (  FORTH-83 АССЕМБЛЕР ДЛЯ К580 ТЕМАШКИНО  1985 )

              VOCABULARY ASSEMBLER

              ASSEMBLER DEFINITIONS

                  (  МАШИННЫЕ КОМАНДЫ  )

          DECIMAL       : 8* 2* 2* 2* ;

          4 CONSTANT H  5 CONSTANT L  7 CONSTANT A  6 CONSTANT PSW

          2 CONSTANT D  3 CONSTANT E  0 CONSTANT B  1 CONSTANT C

          6 CONSTANT M  6 CONSTANT SP

           : 1MI  ( ---> ) CREATE C, DOES> C@ C, ;

           : 2MI  ( ---> ) CREATE C, DOES> C@ + C, ;

           : 3MI  ( ---> ) CREATE C, DOES> C@ SWAP 8* + C, ;

           : 4MI  ( ---> ) CREATE C, DOES> C@ C, C,   ;

           : 5MI  ( ---> ) CREATE C, DOES> C@ C,  ,   ;

          HEX

          00 1MI NOP   76 1MI HLT   F3 1MI DI    FB 1MI EI

          07 1MI RLC   0F 1MI RRC   17 1MI RAL   1F 1MI RAR

          E9 1MI PCHL  F9 1MI SPHL  E3 1MI XTHL  EB 1MI XCHG

          27 1MI DAA   2F 1MI CMA   37 1MI STC   3F 1MI CMC

          80 2MI ADD   88 2MI ADC   90 2MI SUB   98 2MI SBB

          A0 2MI ANA   A8 2MI XRA   B0 2MI CRA   B8 2MI CMP

          09 3MI DAD   C1 3MI POP   C5 3MI PUSH  02 3MI STAX

          0A 3MI LDAX  04 3MI INR   05 3MI DCR   03 3MI INX

          0B 3MI DCX   C7 3MI RST   D3 4MI OUT   DB 4MI IN

          C6 4MI ADI   CE 4MI ACI   D6 4MI SU1   DE 4MI SBI

          E6 4MI ANI   EE 4MI XRI   F6 4MI ORI   FE 4MI CPI

          22 5MI SHLD  2A 5MI LHLD  32 5MI STA   3A 5MI LDA

          C4 5MI CNZ   CC 5MI CZ    D4 5MI CNC   DC 5MI CC

          E4 5MI CPO   EC 5MI CPE   F4 5MI CP    FC 5MI CM

          CD 5MI CALL  C9 1MI RET   C3 5MI JMP

          C0 1MI RNZ   C8 1MI RZ    D0 1MI RNC   DB 1MI RC

          E0 1MI RPO   E8 1MI RPE   F0 1MI RP    F8 1MI RM

          C2 5MI JNZ   CA 5MI JZ    D2 5MI JNC   DA 5MI JC

          E2 5MI JPO   EA 5MI JPE   F2 5MI JP    FA 5MI JM

          : MOV SWAP 8* 40 + + C, ;

          : MVI SWAP 8* 6  + C, C, ;

          : LXI SWAP 8* 1 + C, , ;

            (  КОДЫ УСЛОВИЙ ДЛЯ СТРУКТУР УПРАВЛЕНИЯ  )

          C2 CONSTANT 0=   D2 CONSTANT CS

          E2 CONSTANT PE  F2 CONSTANT 0<

            (  СТРУКТУРЫ УПРАВЛЕНИЯ  )

          DECIMAL

          : NOT    ( N:КОД УСЛОВИЯ ---> N1) 8 + ;

          : THEN   ( АДР,2 ---> ) 2 ?PAIRS HERE SWAP ! ;

          : IF     ( КОД ---> АДР,2 ) C, HERE 0 , 2 ;

          : ELSE   ( АДР,2 ---> АДР1,2) 2 ?PAIRS C3 IF ROT SWAP THEN  2 ;

          : BEGIN  ( ---> АДР,1 )  HERE 1 ;

          : UNTIL  ( АДР,1,КОД --->) SWAP 1 ?PAIRS С, , ;

          : AGAIN  ( АДР,1 --->) 1 ?PAIRS C3 C, , ;

          : WHILE  ( АДР,1,КОД ---> АДР,1,АДР1,4)  IF 2 + ;

          : REPEAT ( АДР,1,АДР1,4 --->) >R >R AGAIN R> R> 2- THEN ;

            (  РАБОТА С МЕТКАМИ  )

          10 CONSTANT LBLMAX ( МАКСИМАЛЬНОЕ ЧИСЛО ЛОКАЛЬНЫХ МЕТОК>

             VARIABLE LTABLE   LBLMAX 1+ 2*  ALLOT

          10 CONSTANT FRMAX ( МАКСИМАЛЬНОЕ ЧИСЛО ССЫЛОК ВПЕРЕД)

             VARIABLE FRTABLE  FRMAX  2* 2*  ALLOT

          : FRCHK   ( ---> ПРОВЕРКА НЕРАЗРЕШЕННЫХ ССЫЛОК ВПЕРЕД )

                    FRMAX 0 DO I 2* 2* FRTABLE + @

                               ABORT" НЕРАЗРЕШЕННАЯ ССЫЛКА ВПЕРЕД"

                            LOOP ;

          : FRCLR   ( ---> ИНИЦИАЛИЗАЦИЯ ТАБЛИЦЫ ССЫЛОК ВПЕРЕД )

                    FRTABLE FRMAX 2* 2* ERASE

                    LTABLE LBLMAX 1+ 2* ERASE ;

          : FRRES   ( N:МЕТКА --->   РАЗРЕШЕНИЕ ССЫЛОК ВПЕРЕД )

                    FRMAX 0 DO I 2* 2* FRTABLE + 2DUP @ =

                              IF HERE OVER 2+ @ +! 0!

                              ELSE DROP THEN

                            LOOP ;

          : FRADD   ( N:МЕТКА ---> ДОБАВЛЕНИЕ ССЫЛКИ ВПЕРЕД В ТАБЛИЦУ )

                    FRMAX 1+ 0

                    DO FRMAX I = ABORT" СЛИШКОМ МНОГО ССЫЛОК ВПЕРЕД"

                       I 2* 2* FRTABLE + DUP @ 0=

                       IF 2DUP ! HERE 1+ SWAP 2+ ! LEAVE ELSE DROP THEN

                    LOOP ;

             (   ОПРЕДЕЛЯЮЩИЕ ВХОЖДЕНИЯ МЕТОК )

          : !LT     CREATE , DOES> @  FRRES HERE SWAP 2* LTABLE + ! ;

                    LOOP ;

             (   ОПРЕДЕЛЯЮЩИЕ ВХОЖДЕНИЯ МЕТОК )

          : !LT     CREATE , DOES> @  FRRES HERE SWAP 2* LTABLE + ! ;

             (   ИСПОЛЬЗУЮЩИЕ ВХОЖДЕНИЯ МЕТОК )

          : @LT     CREATE , DOES> @ DUP 2*

                    LTABLE + @ SWAP OVER 0=

                    IF FRADD THEN DROP ;

          1 !LT 1#:   2 !LT 2#:  3 !LT 3#:  4 !LT 4#!  5 !LT  5#:

          6 !LT 6#:   7 !LT 7#:  8 !LT 8#:  9 !LT 9#! 10 !LT 10#:

          1 @LT 1#    2 @LT 2#   3 @LT 3#   4 @LT 4#   5 @LT  5#

          6 @LT 6#    7 @LT 7#   8 @LT 8#   9 @LT 9#  10 @LT 10#

               (   ПЕРЕКЛКЮЧЕНИЕ В АССЕМБЛЕР  )

                      FORTH DEFINITIONS '

          : BEG-ASM [ ASSEMBLER ] FRCLR [ FORTH ] ASSEMBLER

                    !CSP ;

          : END-ASM [ ASSEMBLER ] FRCHK [ FORTH ] ?CSP

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

Машинные команды на языке ассемблера записываются в обратной польской форме, принятой для языка Форт: <операнды> <операция>, где <операнды> - слова, вычисляющие на стеке размещения операндов данной машинной команды, а <операция> - ее мнемоника. Для обозначения регистров микропроцессора К580 зарезервированы слова А В С D E H L, для задания регистровых пар используются слова В D H SP PSW, для косвенной адресации через регистровую пару HL - слово М. При исполнении слов, обозначающих регистры, регистровые пары и косвенную адресацию, на стеке оставляются значения, соответствующие принятым в системе команд К580 обозначениям для регистров и регистровых пар:

          A  B  C  D  E  H  L  M  SP  PSW

          7  0  1  2  3  4  5  6   6   6

При исполнении мнемоники машинной команды на вершину словаря компилируется соответствующая машинная инструкция. Размещения операндов при этом снимаются со стека. Порядок вычисления операндов машинной команды соответствуют порядку написания операндов на обычном языке ассемблера.

Ниже приведены для сравнения записи машинных команд на обычном языке ассемблера и на языке ассемблера данной форт-системы:

          HLT              HLT

          DAD  SP          SP DAD

          PUSH PSW         PSW PUSH

          CMP  C           C CMP

          MOV  A,M         A M MVI

          MVI  B,2         B 2 MVI

          LXI  0,120+8     D 120 8 + LXI

          CALL 5002H       HEX 5002 CALL DECIMAL

Для определения мнемоник используются определяющие слова, соответствующие форматам машинных команд (1MI, ..., 5MI). Все они имеют одинаковую создающую часть, которая компилирует в поле параметров статьи для мнемоники однобайтный код маски для кода данной команды, который снимается со стека. Исполняющая часть определений, используя маску и размещения операндов, которые она снимает со стека, компилирует двоичный код, соответствующий данной команде. Так, например, через слово 1MI определяются машинные команды, не имеющие операндов. При исполнении мнемоники такой команды на вершину словаря компилируется однобайтный код операции. Слово 3MI определяет мнемоники команд, имеющих один операнд - номер регистра. Этот номер занимает разряды со 2 по 4 в однобайтном коде команды. Поэтому исполняющая часть сдвигает свой операнд - номер регистра - влево на 3 разряда исполнением слова 8* и добавляет к нему маску команды. Получившийся однобайтный код компилируется на вершину словаря. Три команды - MOV, MVI и LXI - не подходят под описанные общие форматы, поэтому они определяются непосредственно через двоеточие.

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

Условный оператор в полной или сокращенной форме записывается следующим образом:

          <КОД-УСЛОВИЯ> IF <ЧАСТЬ-ТО> ELSE <ЧАСТЬ-ИНАЧЕ> THEN

          <КОД-УСЛОВИЯ> IF <ЧАСТЬ-ТО> THEN

Часть "то" выполняется, только если в разрядах PSW установлен заданный "код условия", в противном случае выполняется часть "иначе", если она есть.

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

          BEGIN <ТЕЛО-ЦИКЛА> AGAIN

          BEGIN <ТЕЛО-ЦИКЛА> <КОД-УСЛОВИЯ> UNTIL

          BEGIN <ТЕЛО-1> <КОД-УСЛОВИЯ> WHILE <ТЕЛО-2> REPEAT

Цикл BEGIN-AGAIN является бесконечным, BEGIN-UNTIL прекращает выполнение при к указанном коде условия, в цикле BEGIN-WHILE-REPEAT, наоборот, указанный код условия задает продолжение цикла.

Для указания кода условия в ассемблере используются следующие слова: 0= - установлен флаг Z, 0< - флаг S, CS - флаг C, PE - флаг P. Для инвертирования кода условия используется слово NOT.

Структурные операторы компилируют на своем месте команды безусловных и условных переходов с указанным условием.

Для работы с явно задаваемыми метками в ассемблер включены слова вида 1#: 2#: ... 10#: и 1#2#...10#. Слова первой группы задают определение метки, слова второй - использование. Метки имеют строго локальный характер, т.е. эти слова можно использовать только внутри определений через CODE и между словами ;CODE и END-CODE. Наряду с метками допускается использование ассемблерных структур управления.

Локальные метки рекомендуется использовать только в трехбайтных командах типа JMP, CALL, LXI. Уже определенные локальные метки могут участвовать в арифметических и стековых операциях; использующие вхождения меток, определяющие вхождения которых вводятся позднее (ссылки вперед), могут участвовать только в операциях, сводящихся к добавлению к ним числа со знаком.

Для реализации работы с метками ассемблер имеет таблицу меток LTABLE и таблицу ссылок вперед FRTABLE. Их размер определяет максимальное число разных меток и ссылок вперед в пределах одного определения в машинном коде (от слова CODE или ;CODE до слова END-CODE). Для каждой возможной метки соответствующий элемент таблицы LTABLE содержит нуль, если метка еще не определена, или адрес, который является ее значением, если метка уже определена. Входом в эту таблицу служит номер метки. В таблице FRTABLE для каждой ссылки вперед хранятся два значения: номер метки и адрес в словаре, по которому нужно вписать ее значение. По этому адресу скомпилировано значение смещения, которое нужно добавить как число со знаком к значению данной метки.

Слова для использующих вхождений меток определяются через @LT, например 1 @LT 1#. Исполнение определенного таким образом слова 1#, обозначающего метку номер 1, состоит в обращении к таблице LTABLE по индексу 1. Если там стоит нуль, т. е. метка номер 1 еще не определена, то этот нуль выдается на стек в качестве смещения от значения метки, которое определится позднее. Одновременно в таблицу FRTABLE заносится запись о данной ссылке вперед. В качестве адреса, куда нужно будет впоследствии вписать значение метки, берется HERE 1+. Здесь используется тот факт, что в микропроцессоре К580 операнд-адрес занимает 2 байта, следующие за однобайтным кодом операции.

Слова для определения меток определяются через слово !LT, например 1 !LT 1#:. Исполнение определенного таким образом слова 1#: определяет метку номер 1 как текущий адрес вершины словаря HERE. Его действие состоит в том, что выполняется просмотр таблицы FRTABLE с разрешением всех накопившихся ссылок на данную метку, после чего запись о данной метке заносится в таблицу LTABLE.

Последнюю группу определений составляют слова для доступа к встроенному ассемблеру при компиляции машинного кода. Помимо стандартных форт-слов CODE, ;CODE и END-CODE здесь определяются слова LABEL и NEXT;. Первое используется для создания именованных подпрограмм, к которым можно обращаться из машинного кода. Второе является сокращением для часто встречающегося окончания ассемблерных определений: NEXT JMP END-CODE. В качестве примера приведем определение слова ><, которое меняет местами байты в переданном на стеке двух-байтном значении:

          CODE ><  ( N1 ---> N2)   H POP

             A L MOV  L H MOV  H A MOV  H PUSH  NEXT ;

Приведенная реализация встроенного ассемблера может быть усовершенствована по нескольким направлениям. Прежде всего, увеличив константы LBLMAX и FRMAX и добавив новые определения для слов, обозначающих метки, можно увеличить количество разных меток и ссылок вперед, которые разрешается использовать в ассемблерном определении. Далее можно ввести контроль правильности операндов, включив соответствующие проверки в исполняющую часть определяющих слов для мнемоник команд. Для проверки числа элементов на стеке обычно используется глобальная ячейка CSP и слово !CSP, которое засылает в нее текущий адрес вершины стека. Интересным расширением является возможность введения макрокоманд. Макрокоманды без локальных меток можно определять обычным образом через двоеточие:

          INRN ( R:РЕГИСТР,H:ЧИСЛО РАЗ ---> )

          [ ABSEMBLER ] 0 DO DUP INR LOOP  DROP ;

При исполнении текста А 3 INRN будут скомпилированы три команды А INR.

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


Приложение 1

Hi!

Приложение 1. Модель форт-системы -- отсутствует. Из-за труднораспознаваемого

шрифта. Вернее сказать из-за объема текста, который мне набрать не по силам.

Благодарности. Михаилу Николаеву за книжку. Мише Войтовичу за сканирование

и распознавание.

О всех замеченых ошибках и неточностях просьба сообщать мне.

--

Dr. Cactus cactus@forth.org.ru


От меня

Я нашел текст этой книги на сайте http://www.forth.org.ru и очень признателен всем, кто внес свои усилия в его оцифровку. Получился очень полезный ресурс для всех кто еще интересуется таким замечательным и уникальным языком программирования как Forth.

В виду того, что я предпочитаю пользоваться компилированными справочными системами в формате CHM (Compiled HTML) я провел немного времени, чтобы преобразовать этот текст в CHM формат. Я думаю, что во время этой работы у меня могли закрасться в текст какие-либо ошибки и опечатки. Поэтому, я очень прошу всех, кто будет пользоваться этим ресурсом и заметит такие неприятные взгляду места, а также если не будет лень, сообщить мне о них по адресу: Ihor_Izvarin@Yahoo.COM. (Это более-менее "местонезависимый" адрес, то есть если я перемещусь в другую компанию, то это адрес останется неизменным.)

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

Заранее всем благодарен.

Редакция 3 от 4 июля 2002 года. (Прямо подарок в День Независимости Соединенных Штатов Америки J)