Руководство по программированию на Форте [Илья Тарасов] (fb2) читать онлайн

- Руководство по программированию на Форте 545 Кб, 67с.  (читать) (читать постранично) (скачать fb2) - Илья Тарасов

Настройки текста:



Илья Тарасов РУКОВОДСТВО ПО ПРОГРАММИРОВАНИЮ НА ФОРТЕ

1. Основные сведения о языке

Язык программирования Форт (Forth) появился в 1970 г. благодаря работам Чарльза Мура. К моменту создания первого транслятора Форта уже существовали языки программирования третьего поколения, к которым относятся, в частности, Си и Паскаль. Язык, разработанный Муром, настолько отличался от них, что был отнесен к языкам программирования четвертого поколения, или Fourth generation level. Однако компьютер, использовавшийся для создания первого транслятора, допускал максимум пять символов в обозначениях, поэтому название Fourth («четвертый») было сокращено до Forth. Быстрый рост популярности Форта приходится на 70-е годы, когда оказалось, что при весьма умеренной сложности разработки транслятора этот язык обеспечивает хорошую альтернативу программированию на языке ассемблера. На протяжении своего существования Форт использовался для решения широкого ряда задач. В настоящий момент наиболее перспективными областями его применения являются управление в автоматизированных системах, в том числе на базе микроконтроллеров, построение интерпретирующих систем и скриптовых языков, создание вычислительных и графических программных пакетов.

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

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

Форт не является просто «еще одним языком», который отличается только экзотическим синтаксисом. В действительности все синтаксические особенности этого языка преследуют одну цель – обеспечить максимально простое построение программного кода. Полноценный транслятор Форта, включающий в себя интерпретатор, компилятор и основные библиотеки, можно создать в пределах десяти килобайт. Полноценные трансляторы для 32-разрядных платформ, включающие в себя расширенные библиотеки и полноценную поддержку возможностей операционной системы, занимают около 30 килобайт. Все это является следствием максимальной простоты используемых при компиляции правил, однако взамен приходится использовать чуть усложненный по сравнению с привычным синтаксис. Следует иметь в виду, что усложненная для программиста запись конструкций языка является в то же время предельно упрощенной для транслятора. В действительности Форт отличается тем, что он ориентирован не на синтаксис, а на удобство выполнения процессором своих команд.

Основу программной модели Форта можно выразить с помощью двух понятий – словарь и отдельный стек данных. Именно они определяют свойства этого языка.

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

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

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

Обычно за сохранением баланса стека следит компилятор, подставляя в вызываемые подпрограммы соответствующие прологовые и эпилоговые части, выполняющие прием аргументов со стека и размещение возвращаемых данных. В некоторых процессорах есть специальные команды, облегчающие построение прологовой и эпилоговой частей. Например, начиная с 80286 в семействе процессоров Intel, были введены команды ENTER и LEAVE.

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


Основное средство ввода новых понятий в Форте – так называемое определение через двоеточие. Это синтаксическая форма, которая позволяет ввести в состав словаря новое слово. Полученное таким образом слово немедленно становится равноправным членом словаря, и может участвовать в создании новых определений. Пример определения через двоеточие приведен ниже:


: НОВОЕ_СЛОВО СЛОВО1 СЛОВО2 СЛОВО3 ;


В приведенном примере было создано слово с именем НОВОЕ_СЛОВО. Именем слова считается фрагмент текста, находящийся непосредственно за двоеточием, за исключением ведущих пробелов. Имя может содержать любые символы (за исключением того же пробела). Вполне допустимо назначать имя, начинающееся с цифры, и даже состоящее из одних цифр. Максимальная длина имени различна в разных реализациях Форта, но все они должны обеспечивать длину как минимум 31 символ. Допустимо и перекрытие имен – можно использовать имя, которое уже имеется в словаре. При этом все дальнейшие обращения к слову с таким именем будут использовать последнее определение (однако ранее созданные слова будут по-прежнему использовать старую версию; подробнее об этом см. главу 8).

Действие, выполняемое определяемым словом, записывается сразу за его именем. Описание действия завершается словом ; (точка с запятой). Необходимо обратить особое внимание на то, что точка с запятой – это именно слово, а не элемент синтаксического оформления. Соответственно, она должна быть отделена хотя бы одним пробелом от предыдущего текста. В приведенном примере действие определяемого слова заключается в последовательном выполнении слов СЛОВО1, СЛОВО2 и СЛОВО3. Имеется в виду, что слова с такими именами уже были определены ранее.

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

При всех достоинствах и недостатках Форт, несомненно, представляет собой весьма интересное направление в программировании. Независимо от уровня практической потребности в написании программ, знание принципов этого языка может оказаться весьма полезным. Кроме того, не следует рассматривать Форт как некий готовый программный продукт, или «черный ящик». Некоторые технологии, принципы взаимодействия программных модулей, интерпретации текста и т.д., могут оказаться полезными для реализации их в программах, разрабатываемых с использованием других языков программирования.

2. Работа со стеком и арифметические операции

Стек – это основной элемент Форта, обеспечивающий передачу данных между отдельными словами. Он представляет собой массив данных, организованный по принципу «первым зашел – последним вышел». Наглядно такую структуру можно представить в виде стопки листов бумаги, когда наверху находится лист, положенный туда последним. Все слова Форта работают с данными, оставленными для них на вершине стека. При этом механизм, с помощью которого данные попали на стек, совершенно неважен. Таким образом, числа на стеке могут рассматриваться как некоторый аналог аргументов, передаваемых функциям таких языков программирования, как Си или Паскаль.

Каждое слово Форта ожидает получить на стеке данные в определенном порядке. Поэтому Форт имеет в своем составе группу слов, предназначенных специально для манипуляций числами на стеке. Эти слова являются своеобразной «визитной карточкой» Форта, поскольку знакомство с языком обычно начинают именно с них, и сложно представить программу, в которой они бы не использовались. Результатом работы слов является изменение состояния стека. Поэтому перед рассмотрением первой группы слов введем понятие стековой нотации. Под этим термином понимается запись состояния стека до выполнения слова и после выполнения. Например, запись ( A, B  C ) означает, что до исполнения некоторого слова на стеке находились числа A и B, причем B было сверху. После исполнения (справа от стрелки), эти числа оказались удалены, а на стек было помещено число C. Содержимое стека ниже числа A при этом не изменяется, в противном случае изменения отражаются в стековой нотации. Скобки являются естественным обрамлением для стековой нотации, поскольку таким образом в программу на Форте вводятся комментарии.

Если в результате работы слова состояние стека может изменяться различным образом, возможные варианты записывают через вертикальную разделительную черту (символ «|»).

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


Имя Стековая нотация Описание
DUP ( A → A, A ) Дублирует верхнее число стека
DROP ( A → ) Удаляет верхнее число со стека
SWAP ( A, B → B, A ) Меняет местами два верхних числа
OVER ( A, B → A, B, A ) Кладет на стек второе сверху число
ROT ( A, B, C → B, C, A ) Вращает три верхних числа в соответствии с приведенной стековой нотацией
DEPTH ( → A ) Возвращает глубину стека – количество чисел на нем до выполнения слова DEPTH
?DUP ( A → A | A, A ) Если верхнее число стека равно нулю, оставляет стек без изменений, иначе дублирует верхнее число стека.

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

Многие реализации Форта поддерживают так называемое расширение ядра (Core Extension). Оно позволяет работать с дополнительными словами:


Имя Стековая нотация Описание
NIP ( A, B → B ) Удаляет из стека второе сверху число, сдвигая вершину на его место
TUCK ( A, B → B, A, B) Копирует верхнее число на третью сверху позицию
PICK ( N → AN ) Копирует на вершину стека N-е сверху число. 0 PICK аналогично DUP
ROLL ( AN, ...A0, N → AN-1,... A0 AN ) Вращает N верхних чисел стека. 1 ROLL аналогично SWAP

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

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


Имя Стековая нотация Описание
RDROP ( R: A → R : ) Удаляет число со стека возвратов
R> ( R : A → A) Перемещает число со стека возвратов на стек данных (со стека возвратов число при этом удаляется)
>R ( A → R : A) Перемещает число со стека данных на стек возвратов (со стека данных число при этом удаляется)
R@ ( R: A → A, R : A ) Копирует число со стека возвратов на стек данных

Буква R в записи R : A означает, что число A находится на стеке возвратов (от Return Stack).

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

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


Имя Стековая нотация Описание
+ ( A, B → A + B ) Заменяет два верхних числа со стека данных их суммой.
( A, B → A - B ) Заменяет два верхних числа со стека данных их разностью.
* ( A, B → A * B ) Заменяет два верхних числа со стека данных их произведением.
/ ( A, B → A / B ) Заменяет два верхних числа со стека данных частным от их деления.
MOD ( A, B → A mod B ) Заменяет два верхних числа со стека данных остатком от их деления.
/MOD ( A, B → A / B, A mod B) Заменяет два верхних числа со стека данных частным и остатком от их деления.
ABS ( A → |A| ) Заменяет число его модулем (абсолютным значением).
NEGATE ( A → –A ) Изменяет знак числа на противоположный.
AND ( A, B → A and B ) Заменяет два верхних числа со стека данных их побитным логическим И.
OR ( A, B → A or B ) Заменяет два верхних числа со стека данных их побитным логическим ИЛИ.
XOR ( A, B → A xor B ) Заменяет два верхних числа со стека данных их побитным логическим ИСКЛЮЧАЮЩИМ ИЛИ.
NOT ( A → not A ) Заменяет число со стека данных его логическим отрицанием.

Из таблицы видно, что все арифметические операции должны выполняться уже после того, как соответствующие числа оказались на стеке. В этом Форт не делает исключений. Результатом же такого правила является необходимость использования постфиксной, или обратной польской, записи формул. Вначале записываются числа в том порядке, как это указано в стековой нотации, а затем идет слово – арифметическая операция. Например, 2 + 2 на Форте будет выглядеть как 2 2 +

Приведем еще несколько примеров записи арифметических выражений:


Обычная запись Постфиксная запись
4 – 3 4 3 –
6 * 7 6 7 *
(2 + 3) * 5 – 6 2 3 + 5 * 6 –

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

Такая форма записи, конечно, непривычна и является одним из главных аргументов при критике Форта. Можно долго перечислять достоинства и недостатки такого подхода, однако следует заметить, что постфиксная запись является не синтаксическим «вывертом», а практически единственной формой представления выражений, пригодной для работы со стеком и словарем – базовыми понятиями Форта, представляющими собой его основу.

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

Можно заметить, что стек данных является целочисленным, поскольку использует такие понятия, как целая часть и остаток от деления. Это означает, что он дает возможность оперировать только с числами формата integer. Разрядность чисел на стеке определяет и разрядность транслятора как такового.

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


Имя Стековая нотация Описание
*/ ( A, B, C → A*B/C ) Выполняет вычисления в соответствии со стековой нотацией.
*/MOD ( A, B, C → A*B/C, A*B mod C) то же

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

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


Имя Стековая нотация Описание
S>D ( A → D ) Преобразует число на вершине стека в число двойной разрядности
D>S ( D → A ) Преобразует число двойной разрядности в обычное число

Для чисел двойной разрядности допустимы следующие операции.


Имя Стековая нотация Описание
D+ ( D1, D2 → D1 + D2 ) Складывает числа двойной длины.
D– ( D1, D2 → D1 – D2 ) Вычитает числа двойной длины.
2DUP ( D → D, D ) Аналог слова DUP для чисел двойной длины
2DROP ( D → ) Аналог слова DROP для чисел двойной длины
2SWAP ( D1, D2 → D2, D1 ) Аналог слова SWAP для чисел двойной длины
2OVER ( D1, D2 → D1, D2, D1 ) Аналог слова OVER для чисел двойной длины
2ROT ( D1, D2, D3 → D2, D3, D1 ) Аналог слова ROT для чисел двойной длины
DABS ( D1 → |D1| ) Модуль числа двойной длины
DNEGATE ( D1 → – D1 ) Изменяет знак числа двойной длины

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

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

Для чисел с плавающей точкой обычно выделяется отдельный стек (хотя стандартом допускается их хранение на стеке данных). Для платформы 80x86 естественно использовать аппаратный стек сопроцессора. Некоторым недостатком является его ограниченный объем – всего 8 ячеек, зато производительность Форта на операциях с плавающей точкой существенно возрастает из-за большей близости языка к аппаратуре.

Основные математические операции для чисел с плавающей точкой приведены ниже:


Имя Стековая нотация Описание
F+ ( F: A, B → F: A + B ) Заменяет два верхних числа со стека с плавающей точкой их суммой.
F– (F: A, B → F: A - B ) Заменяет два верхних числа со стека с плавающей точкой их разностью.
F* (F: A, B → F: A * B ) Заменяет два верхних числа со стека с плавающей точкой их произведением.
F/ (F: A, B → F: A / B ) Заменяет два верхних числа со стека с плавающей точкой частным от их деления.
FABS (F: A → F: |A| ) Возвращает модуль числа.
FNEGATE (F: A → F: –A ) Изменяет знак числа.
FSQRT (F: A → F: sqrt A ) Возвращает квадратный корень из числа.
FSIN (F: A → F: sin A ) Возвращает синус числа.
FCOS (F: A → F: cos A ) Возвращает косинус числа.
FEXP (F: A → F: eA ) Возвращает величину eA.
FLN (F: A → F: lnA ) Возвращает натуральный логарифм числа.

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

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


Имя Стековая нотация Описание
FDUP ( F: A → F: A, A ) Дублирует верхнее число стека с плавающей точкой
FDROP (F: A → ) Удаляет верхнее число со стека с плавающей точкой
FSWAP (F: A, B → F: B, A ) Меняет местами два верхних числа на стеке с плавающей точкой
FOVER (F: A, B → F: A, B, A ) Кладет на стек с плавающей точкой второе сверху число
FROT (F: A, B, C → F: B, C, A ) Вращает три верхних числа стека с плавающей точкой в соответствии с приведенной стековой нотацией
FDEPTH ( → A ) Возвращает глубину стека с плавающей точкой, число будет помещено на стек данных

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


Имя Стековая нотация Описание
S>F ( F: A → A ) Переносит число со стека данных на стек с плавающей точкой.
F>S ( A → F: A ) Переносит число со стека с плавающей точкой на стек данных.
D>F ( D → F: A ) Переносит число двойной длины со стека данных на стек с плавающей точкой.
F>D ( F: A → D ) Переносит число со стека с плавающей точкой на стек данных, формируя число двойной длины.

Эти слова выполняют преобразования формата, поэтому используйте их осторожно. Например, 5.1 F>S S>F даст в итоге 5, поскольку при переносе на стек данных было выполнено округление числа.

На основании изученных слов Форта попытаемся рассмотреть пример работы с транслятором. Для этого осталось сделать последний штрих – рассмотреть слова, позволяющие узнать результат вычислений. Ввиду того, что печать числа, находящегося на вершине стека – операция весьма частая, для этой цели используется чрезвычайно короткое слово – . (точка). Соответственно, для печати числа с плавающей точкой используется слово F. (в этом случае вывод произойдет в экспоненциальной форме, то есть в виде x.xxxxEyy; стандартом предусмотрено также слово FE. , выводящее результат в более привычном «инженерном» формате).

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

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

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


: KVADRAT ( A --> A^2 ) DUP * ;


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

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


: PIFAGOR^2 ( A, B --> A^2+B^2)

    KVADRAT            \ A, B^2

    SWAP               \ B^2, A

    KVADRAT            \ B^2, A^2

    +

;


Обратите внимание на слово SWAP между двумя словами KVADRAT. Нам требуется обеспечить нужный порядок чисел на стеке, а после выполнения первого слова KVADRAT на вершине стека окажется квадрат одного из катетов. Выполнение SWAP отправляет его вниз, помещая на вершину второй катет. Теперь мы можем сложить квадраты катетов, получив квадрат гипотенузы.

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


3 4 PIFAGOR^2 .


В результате на экране будет напечатан результат: 25.


Полученные слова, KVADRAT и PIFAGOR^2 можно теперь использовать в любых последующих определениях. Поскольку они, наравне с базовыми словами Форта, работают со стеком данных, никакой разницы не будет заметно. Вновь вводимые понятия не являются ни процедурами, ни функциями, ни чем-либо еще, принципиально отличающимся от базового синтаксиса языка. Программист может создать удобный для него набор математических операций с привычным для себя синтаксисом. Обратите внимание на использование специальных символов в слове PIFAGOR^2. Большинство реализаций Форта позволят определить и слово ПИФАГОР^2, использующее русские буквы в названии. Для имитации стиля С/С++ и подобных процедурных языков можно использовать и вариант Pifagor^2(A,B). Следует, правда, учесть, что скобки и названия переменных в них не являются самостоятельными объектами – транслятор Форта будет воспринимать всю эту запись целиком.

Точно так же можно реализовать вычисление гипотенузы, используя числа с плавающей точкой.


: FKVADRAT FDUP F* ;

: FPIFAGOR FKVADRAT FSWAP FKVADRAT F+ FSQRT ;


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


3.00E0 4.00E0 FPIFAGOR FE.


Результатом будет 5.0000000. В этом примере мы использовали экспоненциальную форму записи исходных чисел. В результате Форт не смог преобразовать введенные фрагменты текста в целочисленный формат, зато наличие десятичной точки и символа E показало, что эти фрагменты могут быть числами с плавающей точкой. Такие преобразования завершились успешно, и на стеке с плавающей точкой оказались нужные нам аргументы. Далее слово FPIFAGOR выполнило над ними необходимые вычисления и мы смогли посмотреть результат словом FE. Можно было бы использовать и F., получив результат в экспоненциальной форме – 5.0000000E0.

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

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

Стандартной конструкцией является условный оператор IF...THEN. Посмотрим, как эта конструкция записывается в Форте.


<условие> IF <условное действие> THEN


При записи условного оператора сохраняется уже известный нам принцип Форта – сначала аргументы, потом операция. Слово IF не исключение – для того, чтобы понять, следует ли исполнять условную часть конструкции, оно должно получить на стеке код условия. Если на стеке находится ИСТИНА, условная часть выполняется, иначе управление передается на точку за словом THEN.

Что представляет собой условие выполнения? По смыслу оно должно принимать одно из двух значений ИСТИНА/ЛОЖЬ и иметь специальный логический (boolean) тип. Форт и эту проблему решает весьма радикально – логическое условие представляет собой просто число на стеке данных. Ноль рассматривается как ЛОЖЬ, все остальные числа – как ИСТИНА. Таким образом, для определения условия выполнения мы можем использовать любое математическое выражение, дающее в результате ноль или «не ноль».

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


Имя Стековая нотация Описание
= ( A, B → ИСТИНА, если A=B ) Заменяет два числа ИСТИНОЙ, если они равны.
> ( A, B → ИСТИНА, если A>B ) Заменяет два числа ИСТИНОЙ, если второе сверху число больше первого.
< ( A, B → ИСТИНА, если A<B ) Заменяет два числа ИСТИНОЙ, если второе сверху число меньше первого.
U> ( A, B → ИСТИНА, если A>B ) То же, что >, но числа рассматриваются как числа без знака.
U< ( A, B → ИСТИНА, если A<B ) То же, что <, но числа рассматриваются как числа без знака.
AND ( A, B → A and B ) Побитное логическое И над двумя числами.
OR ( A, B → A or B ) Побитное логическое ИЛИ над двумя числами.
XOR ( A, B → A xor B ) Побитное логическое ИСКЛЮЧАЮЩЕЕ ИЛИ над двумя числами.
NOT ( A → not A ) Логическое отрицание (ЛОЖЬ заменяется на ИСТИНУ и наоборот)

Обратите внимание на беззнаковые версии операций сравнения. Обычно Форт использует знаковое представление чисел, в котором старший бит в двоичной записи числа указывает, что число отрицательное. В таком случае 0FFFFH < 1, поскольку 0FFFFH представляет собой –1 в дополнительной двоичной арифметике. В то же время операция U< рассмотрит оба числа как числа без знака, и на запрос -1 1 U< вернет ЛОЖЬ.

В соответствии со стандартом логические операции выполняются над отдельными битами исходных чисел. Таким образом, 1 (двоичное 00000001) и 4 (двоичное 00000100) при выполнении над ними операции AND дадут 0, поскольку положения установленных разрядов в этих числах не совпадают. С другой стороны, оба этих числа представляют собой ИСТИНУ, так что логическое И над ними должно также давать ИСТИНУ. Чтобы избежать подобных ситуаций, для представления истинных выражений используется –1, имеющее единицы во всех двоичных разрядах. Базовые слова Форта используют именно такое представление логической ИСТИНЫ, и оно настоятельно рекомендуется для использования в словах, добавляемых пользователем.

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


: РАВЕНСТВО ( A, B --> )

    = IF .” Числа равны” THEN

;


Определенное слово РАВЕНСТВО использует знак сравнения (=), который снимает два числа со стека и кладет ИСТИНУ, если они равны. В результате перед выполнением слова IF на стеке оказался код условия. В случае, если числа были равны, будет выполнена часть определения вплоть до слова THEN, то есть напечатан текст «Числа равны».

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

Теперь приведем еще одну версию условного оператора.


<условие> IF <выполняется> ELSE <не выполняется> THEN


И соответствующий этой форме записи пример.


: РАВЕНСТВО ( A, B --> )

    = IF .” Числа равны” ELSE .” Числа не равны” THEN

;


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

Обычные циклы имеют в Форте две формы записи.


BEGIN <условие завершения> UNTIL

BEGIN <условие продолжения> WHILE <операции> REPEAT


Здесь <условие завершения> и <условие продолжения> – последовательности слов Форта, возвращающие на стеке код завершения либо продолжения выполнения. В первом случае выполнение цикла прекратится, когда перед выполнением слова UNTIL на стеке будет находиться любое ненулевое число, а во втором – когда перед словом WHILE на стеке окажется ноль. Естественно, ничто не запрещает выполнять после BEGIN любые действия, помимо вычисления кода условия.

Как и с условным оператором, использующим IF THEN в обоих своих формах, циклы используют слово BEGIN, которое создает в общем-то различные управляющие конструкции.

Циклы со счетчиком формируются в Форте следующими словами:


Имя Стековая нотация Описание
DO ( N2, N1 → ) Начинает цикл со счетчиком от N1 до N2.
LOOP ( → ) Переходит к новой итерации, прибавляя к счетчику 1.
+LOOP ( N → ) Переходит к новой итерации, прибавляя к счетчику N.
I ( → I ) Кладет на стек текущее значение счетчика цикла.
J ( → J ) Кладет на стек текущее значение счетчика объемлющего цикла.
UNLOOP ( → ) Досрочно выходит из цикла.

Как видно из таблицы, цикл со счетчиком всегда начинается словом DO, которое снимает со стека границы счетчика. На стеке они должны находиться в обратном порядке – начальное значение сверху, конечное под ним. При определении границ счета используется следующее правило: цикл прекращается, когда значение счетчика пересекло границу между N2 и N2–1. Иными словами, следующий цикл исполнится 10 раз, со значениями счетчика 0, 1, 2,... 9.


10 0 DO I . LOOP


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

Рассмотрим еще один пример.


0 10 DO I . –1 +LOOP


Здесь переход к следующей итерации происходит с помощью слова +LOOP. Оно снимает со стека требуемое приращение счетчика цикла (которое может быть и не константой). В приведенном примере пересечение границы между N2 и N2–1 произойдет при переходе от 0 к –1. Таким образом, цикл выполнится 11 раз со значениями 10, 9, ... 0.

Действие слова UNLOOP видимо, понятно. Оно позволяет досрочно выйти из цикла со счетчиком, передав управление на слово, следующее за LOOP. Например, следующий цикл прекратит выполнение, когда значение счетчика станет равно 5.


10 0 DO I 5 = IF UNLOOP THEN LOOP


Что же представляют собой слова I и J? Мы уже говорили, что Форт не выделяет особо ни одну из категорий языка. В его составе есть переменные, но слова I и J на их роль не подходят, поскольку в наших примерах они нигде не были объявлены. В действительности это просто слова, действие которых объясняется способом создания Фортом цикла со счетчиком. При создании этого цикла Форт запоминает начальное и конечное значения счетчика на стеке возвратов, вместе с адресом, на который нужно передать управления для возврата к началу итерации. Таким образом, при исполнении цикла со счетчиком на стеке возвратов находятся три «лишние» по отношению к нормальному выполнению программы числа. Одно из них и представляет собой текущее значение счетчика, которое копируется на стек данных словом I. Аналогично слово J копирует на стек значение счетчика цикла, которое находится на стеке возвратов непосредственно под верхней тройкой чисел.


10 0 DO

   I .

   10 0 DO

      I J * .

   LOOP

LOOP


Этот пример напечатает таблицу умножения, предваряя ее каждый раз номером строчки. Первая строка 10 0 DO создаст на стеке возвратов тройку чисел, характеризующих параметры первого цикла – адрес начала, конечное и текущее значения счетчика. Доступ к текущему значению производится словом I. Однако после создания следующего уровня цикла со счетчиком слово I будет давать уже значение счетчика внутреннего цикла. Для того, чтобы «добраться» до внешнего счетчика, и используется слово J.

Некоторые реализации определяют также слово K, служащее для доступа к третьему объемлющему циклу.

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

4. Создающие определения

В предыдущих главах мы уже столкнулись с несколько странным поведением некоторых слов и конструкций управления. Например, слово .” (точка-кавычка) печатает текст, который находится ПОСЛЕ него, хотя в первых главах уже упоминалось, что все аргументы должны находиться на стеке до исполнения слова. Так же неочевидно поведение управляющих конструкций. Создается впечатление, что они занимаются анализом текста и выполняют то или иное действие в зависимости от выбранной нами формы записи. Такой подход, вобщем-то, вполне ожидаем, поскольку практически все языки имеют в своем составе более или менее сложный синтаксический анализатор.

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

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

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

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

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

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

Исходя из этого, действие слова «двоеточие» должно быть гораздо проще, чем представлялось на первый взгляд. Оно просто создает в словаре новое «пустое» слово, взяв его имя из анализируемой строки. Таким образом, то, что стоит после двоеточия, до транслятора просто не доходит, и мы можем не опасаться появления сообщения «слово не найдено». После создания нового слова двоеточие переключает Форт в состояние компиляции (записью 1 в переменную STATE) и прекращает свое исполнение. Далее все действия по созданию кода производит сам транслятор Форта, последовательно компилируя вызовы слов, встречающихся в исходном тексте.

В таком описании явно просматривается ловушка. Действительно, один раз войдя в состояние компиляции, Форт никогда из него не выйдет, продолжая компилировать все, что будет введено с клавиатуры, в том числе и слово «точка с запятой», завершающее определение слова. Чем же ограничить длительность состояния компиляции? Сделать так, чтобы точка с запятой принудительно выводила транслятор в состояние исполнения? Но структуры управления, такие как IF THEN BEGIN и т.д. также должны сами формировать программный код, т.е. исполняться. Кроме того, программист может и сам ввести структуры управления.

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

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


: MY-IMMEDIATE-WORD ......... ; IMMEDIATE


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

В Форте есть еще два слова, управляющих состоянием транслятора.


Имя Описание
[ устанавливает состояние исполнения (имеет признак IMMEDIATE).
] устанавливает состояние компиляции (поскольку выполняется из состояния исполнения, признак IMMEDIATE ему не требуется.

Приведенные слова можно использовать внутри определений. Рассмотрим следующий пример.


: SCREEN-SIZE 640 480 * ;


Оно дает число точек на экране при разрешении 640x480. Это число представляет собой константу, так что оно может быть вычислено один раз на этапе компиляции и подставлено непосредственно в программный код (именно так поступят современные компиляторы таких языков, как Си и Паскаль). Однако Форт, который не производит никаких действий, за исключением явно указанных программистом, сделает именно то, что было указано в определении – сформирует код, который при исполнении будет класть на стек числа 640 и 480, а затем выполнять их перемножение. Однако можно заставить Форт выполнять вычисление сразу на этапе создания слова. Для этого переведем транслятор в состояние исполнения перед вычислением произведения:


: SCREEN-SIZE [ 640 480 * ] LITERAL ;


После исполнения открывающей квадратной скобки транслятор перешел в состояние исполнения и начал выполнять вводимый текст. На стеке оказались числа 640 и 480, затем выполнилась операция умножения, дав нужный нам результат. Закрывающая квадратная скобка вернула транслятор в состояние компиляции. Что же сделало новое слово LITERAL? Оно имеет признак немедленного исполнения и занимается компиляцией чисел (а точнее – литералов), то есть формированием программного кода, который при своем исполнении положит на стек нужное число. Входные данные для LITERAL – число на стеке (как обычно).

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

Однако наличие признака немедленного исполнения не означает, что такое слово нельзя скомпилировать. Для создания своих структур управления программисту может потребоваться уже готовый фрагмент кода, выполняемый такими словами, как IF, THEN и им подобными. Чтобы обеспечить доступ к словам немедленного исполнения, стандартом предусмотрено слово POSTPONE (в старых версиях имеется его аналог [COMPILE]), которое принудительно компилирует следующее за ним слово, независимо от наличия у него признака немедленного исполнения. Естественно, это слово само имеет признак немедленного исполнения.

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

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

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


CREATE <создающая часть> DOES> <исполняющая часть>


Слово CREATE как и все слова, выполняет очень простое действие. Оно выбирает из введенного текста следующий фрагмент текста и создает в словаре слово с таким именем. В этом его действие полностью совпадает с двоеточием. Однако затем CREATE приписывает вновь введенному слову небольшой программный код. Действие этого кода заключается в помещении на стек адреса первой свободной ячейки памяти на момент создания слова (то есть фактически некоторой константы). Целесообразность такого действия сейчас станет ясна.

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

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

Узнать первый свободный адрес можно, выполнив слово HERE («здесь»). Оно кладет на стек адрес первой свободной ячейки памяти. В случае с раздельным хранением кода и данных этот адрес относится к данным, а свободный адрес в сегменте кода можно узнать, выполнив [C]HERE.

Слова ALLOT и [C]ALLOT управляют распределением памяти. Эти слова имеют похожее действие – снимают со стека число байт, которые требуется зарезервировать в памяти (данных или кода соответственно). Резервирование памяти заключается в простом прибавлении этого числа к текущему адресу свободной ячейки. Однако адрес резервируемой области памяти этими словами нигде не сохраняется.

Таким образом, чтобы создать область памяти для своих целей, необходимо запомнить адрес первой свободной ячейки памяти, а затем зарезервировать требуемый объем. Что касается резервирования, то оно может быть выполнено словом ALLOT, а запоминание адреса, как было указано выше, выполняется словом CREATE. Следующий пример создает область памяти размером 1000 байт.


CREATE MASSIV 1000 ALLOT


Первая часть текста создает в словаре новое слово с именем MASSIV. Это слово при своем исполнении положит на стек адрес первого свободного байта в памяти данных. Теперь необходимо зарезервировать память, что мы и делаем фрагментом 1000 ALLOT.

Слово CREATE создает в словаре новое слово с фиксированным и заранее определенным действием. Однако кроме простого помещения на стек адреса области памяти от слова могут потребоваться какие-то другие действия. Например, константа должна класть на стек не адрес памяти, а сразу его содержимое. Для такого действия Форт имеет слово @ (читается «at»), которое снимает со стека адрес ячейки памяти и кладет содержимое этого адреса. Из памяти читается сразу 16- или 32-разрядное число, в зависимости от разрядности стека. Такая операция называется разыменованием. Таким образом, было бы желательно после помещения на стек адреса, по которому хранится константа, произвести разыменование, то есть к стандартному действию слова, созданному с помощью CREATE, приписать какие-то дополнительные действия. Именно это и выполняет слово DOES>.

Рассмотрим, как в Форт может быть введено понятие константы (обычно константа является зарезервированным понятием любого языка, но в Форт, оказывается, она вводится).


: CONSTANT CREATE , DOES> @ ;


Итак, сейчас в словарь было добавлено слово CONSTANT. Оно позволит определять константы, например, с помощью текста.


1 CONSTANT ОДИН


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

Проанализируем действия, которые будут выполнены при исполнении текста 1 CONSTANT ОДИН. Ни CREATE, ни DOES> не являются словами немедленного исполнения, так что при определении CONSTANT никаких дополнительных действий произведено не было.

Само слово CONSTANT начало свое исполнение, имея единицу на стеке. При входе в CONSTANT начались последовательные вызовы слов, входящих в его определение. Вначале выполнилось слово CREATE, которое должно выбрать фрагмент текста и создать в словаре слово с таким именем. В нашем случае за словом CONSTANT следует ОДИН. Именно такое слово будет создано в словаре. Ему будет приписано стандартное действие – положить на стек адрес свободной ячейки памяти. Затем будет выполнено слово «запятая». Оно перенесет число 1 со стека в память (заметьте, что это как раз тот адрес, который будет положен на стек словом ОДИН). Далее выполняется DOES>, которое начинает модифицировать код, выполняемый словом ОДИН. После DOES> записано @, которое и будет добавлено к стандартному действию. Слово @ заменяет число на стеке содержимым памяти по этому адресу. Таким образом, ОДИН при своем исполнении сначала положит на стек адрес, а потом выполнит разыменование. А по адресу, запомненному словом ОДИН, находится как раз то число, которое было выбрано со стека! Таким образом, выполнение слова ОДИН будет всегда давать в результате 1. Аналогично, определение 5 CONSTANT ПЯТЬ даст нам константу со значением 5.

Таким образом, с использованием конструкции CREATE DOES> появляется возможность создания определяющих слов, позволяющих реализовать объекты с практически любым набором действий.

5. Переменные

В предыдущей главе было показано, как можно создавать собственные определяющие конструкции в Форте. Сам Форт уже имеет среди базовых слов такие конструкции, заранее определенные с помощью механизма CREATE DOES>. К ним относится уже рассмотренное слово CONSTANT, а также слово VARIABLE, создающее свободную область памяти, достаточную для хранения одного числа со стека. При выполнении слова, созданного с помощью VARIABLE (переменная), на стек кладется адрес, соответствующий этой переменной. Этот адрес может быть разыменован, или использован для записи в него нового числа. Таким образом, при работе с Фортом определение адреса памяти и выполнение каких-то действий с ним – разные этапы работы программы. Первый этап выполняется словом-переменной, а последующие целиком определяются программистом. Форт не может самостоятельно присвоить переменной новое значение – он лишь сообщает адрес этой переменной. С учетом того, что с помощью механизма CREATE DOES> программист может создавать конструкции с довольно экзотическим поведением, такой подход представляется вполне оправданным, поскольку неизвестно, что именно необходимо сделать с адресом памяти. И вместо того, чтобы работать с переменными, Форт предоставляет возможность работать непосредственно с памятью. Некоторые слова для работы с памятью, касающиеся ее выделения, были уже рассмотрены.


Имя Стековая нотация Описание
@ ( A → mem A ) Число на стеке, представляющее адрес памяти, заменяется содержимым памяти по этому адресу.
! ( DATA, ADR → ) По адресу ADR записывается число DATA.
C@ ( A → mem A ) То же, что @, но из памяти читается один байт, расширяемый нулями до требуемой длины.
C! ( DATA, ADR → ) То же, что !, но модифицируется только один байт, берущийся из младшего байта числа DATA.
+! ( DATA, ADR → ) То же, что !, но число DATA добавляется к текущему содержимому ячейки памяти.
, ( A → ) Переносит число со стека данных в память по первому свободному адресу.

В случае разделения памяти данных и памяти кода соответствующие версии слов для работы с памятью кода обычно начинаются с [C]. Например, [C]@, [C]C! и т.д.

Таким образом, работа с переменной может происходить следующим образом.


VARIABLE ПЕРЕМЕННАЯ   \ создание переменной

2 ПЕРЕМЕННАЯ !        \ запись значения

ПЕРЕМЕННАЯ @          \ помещение числа

                      \ из переменной на стек


Аналогичные действия можно выполнить над переменными с плавающей точкой.


Имя Стековая нотация Описание
F@ ( A → F: mem A ) Число на стеке, представляющее адрес памяти, удаляется, а на стек с плавающей точкой кладется содержимое памяти по этому адресу.
F! ( ADR, F: DATA → ) По адресу ADR записывается число с плавающей точкой DATA.
F, ( F: DATA → ) Переносит число со стека с плавающей точкой в память по первому свободному адресу.
SF@ ( A → F: mem A ) Число на стеке, представляющее адрес памяти, удаляется, а на стек с плавающей точкой кладется содержимое памяти по этому адресу (в «коротком вещественном» формате).
SF! ( ADR, F: DATA → ) По адресу ADR записывается число с плавающей точкой DATA (в «коротком вещественном» формате).
SF, ( F: DATA → ) Переносит число со стека с плавающей точкой в память по первому свободному адресу (в «коротком вещественном» формате).
DF@ ( A → F: mem A ) Число на стеке, представляющее адрес памяти, удаляется, а на стек с плавающей точкой кладется содержимое памяти по этому адресу (в «двойном вещественном» формате).
DF! ( ADR, F: DATA → ) По адресу ADR записывается число с плавающей точкой DATA (в «двойном вещественном» формате).
DF, ( F: DATA → ) Переносит число со стека с плавающей точкой в память по первому свободному адресу (в «двойном вещественном» формате).

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

Слова F@ F! и F, являются словами общего назначения и работают с форматом, принятым в данной Форт-системе в качестве основного формата чисел с плавающей точкой. В противоположность им, слова с префиксами S и D явно указывают используемый формат. «Короткое вещественное» число занимает в памяти 4 байта, а «двойное вещественное» – 8.


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


CREATE MASSIV[] 20 4 * ALLOT \ резервируем память


: X^2

   20 0 DO               \ для каждой ячейки памяти

      I DUP *            \ вычислили квадрат (данные)

      I 4 * MASSIV[] +   \ вычислили адрес памяти (смещение

                         \ плюс начальный адрес)

      !                  \ произвели запись

   LOOP

;


: X^3

   20 0 DO

      I DUP DUP * *      \ единственное отличие от

                         \ предыдущего слова

      I 4 * MASSIV[] + !

   LOOP

;


: ПРОСМОТР

   20 0 DO

      I .                 \ печатаем номер точки

      .” =”               \ строка «=»

      I 4 * MASSIV[] +    \ вычисляем адрес

      @ .                 \ берем число и печатаем его

      LOOP

;


Теперь мы можем просмотреть состояние памяти словом ПРОСМОТР. Для его использования мы должны вначале выбрать функцию, которую будем просматривать. В нашем распоряжении два слова, выполняющие запись значений функции в рабочий массив. Любое из них может быть выполнено до просмотра. Таким образом, текст X^2 ПРОСМОТР выведет на экран квадраты первых двадцати чисел, а X^3 ПРОСМОТР – кубы тех же чисел.

Понимание принципов работы с памятью, в частности, вычисления адреса переменной, является очень важным для успешной работы с Фортом. Переменные, создаваемые через VARIABLE, сразу дают тот адрес, по которому можно выполнять запись или чтение. В случае с массивом ситуация несколько более сложная, поскольку нельзя предусмотреть все возможные слова для работы с областями памяти произвольного размера. Вместо этого используется подход, проиллюстрированный приведенным примером. Создается слово, при исполнении кладущее на стек начальный адрес массива, а для массива резервируется память необходимого размера. Если выделенный массив достаточен для хранения 20 4-х байтных чисел, то i-е число такого массива хранится по адресу ADR0+i*4, где ADR0 – начальный адрес массива. Именно этот адрес вычисляется с помощью строки I 4 * MASSIV[] + в приведенном примере. Такие фрагменты текста довольно характерны для Форта.

Слово MASSIV[] заканчивается специфическими квадратными скобками. В обычных языках программирования они используются для обрамления индекса массива. Форт уже использует квадратные скобки для управления процессом трансляции текста, но дело не в «занятом» синтаксисе. В действительности запись MASSIV[4,5] в Форте бессмысленна, поскольку потребует достаточно мощного синтаксического анализа, разделения такой строки на отдельные элементы, проверки размерности массива и тому подобных действий. Для любого языка массив – заранее предусмотренный элемент со своим синтаксисом, для Форта же – всего лишь один из вариантов использования конструкции CREATE DOES>. Поэтому все операции по программной поддержке созданных объектов Форт перекладывает на программиста. Квадратные скобки в имени никоим образом не изменяют действие создаваемого слова, но их наличие помогает программисту помнить, что он имеет дело с начальным адресом массива. Имеется еще несколько подобных приемов, позволяющих выбирать «выразительный» синтаксис.

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


Имя Стековая нотация Описание
CMOVE ( A1, A2, LEN → ) Копирует LEN байт из области памяти с начальным адресом A1 в область памяти с начальным адресом A2.
CMOVE> ( A1, A2, LEN → ) То же, что CMOVE, но копирование начинается с конца (служит для копирования перекрывающихся областей памяти).

Анализируя работу с переменными и константами, можно заметить, что занятие это достаточно утомительное. Необходимо постоянно помнить о необходимости разыменования переменных, поскольку одной из наиболее распространенных ошибок при программировании на Форте является именно отсутствие разыменования словом @. Форт не запретит складывать адреса вместо данных по этим адресам (он вообще ничего не запрещает). Таким образом, строка C=A+B на Форте будет выглядеть как A @ B @ + C ! (не следует забывать и о разделяющих пробелах). В то же время константа сразу кладет на стек свое значение, но она не подлежит изменению.

Промежуточным вариантом является использование QUAN-переменных (от англ. Quantity – количество). Эти объекты также строятся на основе CREATE DOES>, но действие слова, созданного по QUAN, заключается в помещении на стек самого значения переменной. При этом теряется возможность записи нового значения в эту переменную стандартным словом !, поскольку адрес переменной получить невозможно.

Для записи нового значения в QUAN-переменную используется конструкция <значение> TO <имя>. Полный набор слов для работы выглядит следующим образом:


QUAN ПЕРЕМЕННАЯ   \ создаем новую переменную

ПЕРЕМЕННАЯ .      \ печатаем ее значение

5 TO ПЕРЕМЕННАЯ   \ записываем новое значение


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

С использованием QUAN-переменных запись математических выражений выглядит гораздо более осмысленно. То же самое C=A+B запишется как A B + TO C.

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

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


Procedure Proc1(x: integer); forward;


Procedure Proc2(y: integer);

begin

   ....

   Proc1(y);

   ....

end;


Procedure Proc1(x: integer);

begin

   Proc2(x);

end;


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

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


VECT PROC


: PROC2

   ...

   PROC

;

: PROC1

   ...

   PROC2

;

‘ PROC1 TO PROC


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

Апостроф – новое слово, которое еще не встречалось в этой книге. Более подробно оно будет рассмотрено позже, а вкратце можно сказать, что выполнение ‘ XXX оставляет на стеке адрес слова XXX, который как раз и ожидается векторным словом. Сразу можно заметить, что это слово не сработает внутри определений, поэтому следует пользоваться его immediate-версией [‘].


: SET-VECTORS [‘] PROC1 TO PROC ;


Имеется возможность отменить назначение, вернув векторному слову начальное состояние. Для этого в составе трансляторов обычно имеется «пустое» слово, сразу же возвращающее управление. Обычно оно называется NOOP («нет операции»). Назначение такого слова ничем не отличается от любого другого назначения.

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

Такое поведение векторных слов позволяет достичь максимальной гибкости при работе с ключевыми понятиями Форта или программы. Например, полезно бывает векторизовать слова, служащие для вывода на экран графики. Если программа может использовать различные видеорежимы, то способы вывода точки на экран могут быть различны – от использования средств BIOS (медленное, но универсальное решение), до прямой записи в видеопамять (значительно более быстрое, но зависящее от видеорежима). Жесткое задание в трансляторе одной из версий существенно ограничит его графические возможности, а смена способа вывода потребует редактирования всей программы. В то же время, если задать векторное слово, например, PIXEL, для которого на стеке ожидаются координаты точки и ее цвет, то можно свободно использовать это слово для организации остальных графических операций – вывода линий, окружностей, закрашенных областей и т.д. В дальнейшем можно определить реализации слова PIXEL для конкретных видеорежимов и назначить одну из них ранее определенному векторному слову. Изменение адреса приведет к тому, что все слова, вызывающие PIXEL, будут перенаправлены им на соответствующую версию, которая и выполнит требуемые действия.

6. Ввод-вывод. Текстовый вывод на экран, ввод с клавиатуры

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

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


Имя Стековая нотация Описание
. ( A → ) Печатает верхнее число со стека данных.
U. ( A → ) Печатает верхнее число со стека данных, рассматривая его как число без знака.
F. ( F: A → ) Печатает верхнее число со стека чисел с плавающей точкой.
FE. ( F: A → ) Печатает верхнее число со стека чисел с плавающей точкой в «инженерном» формате.

Следует отдельно отметить, что встроенный алгоритм печати чисел позволяет выводить их в любой системе счисления от 2 до 36. Ограничение в 36 установлено исключительно из-за того, что в латинском алфавите, символы которого используются для кодирования цифр выше 9, имеется всего 26 букв.

Текущая система счисления хранится в системной переменной BASE. Запись нового значения туда осуществляется точно так же, как и для остальных переменных. Сразу же после записи новой системы счисления все выводимые числа будут использовать именно ее.

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


Имя Стековая нотация Описание
DECIMAL ( → ) Устанавливает десятичную систему счисления
HEX ( → ) Устанавливает шестнадцатеричную систему счисления.
OCTAL ( → ) Устанавливает восьмеричную систему счисления.

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


DECIMAL

: USE-HEX HEX FF + ;


В этом определении мы попытались переключить систему счисления, чтобы получить возможность представить константу FFH=25510. Однако Форт сообщил об ошибке «Слово FF не найдено». Произошло это потому, что вместо переключения системы счисления транслятор скомпилировал такое переключение для исполнения его в будущем. Попытка же разобрать текст FF привела к ошибке, поскольку система счисления осталась прежней.

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


HEX

: USE-HEX FF + ;


То же самое можно выполнить и путем временного перевода Форта в состояние исполнения.


DECIMAL

: USE-HEX [ HEX ] FF + ;


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

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


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

Строка не является для Форта встроенным понятием и представляет собой простой массив, хранящий отдельные символы. Для печати, соответственно, необходимо указать адрес начала данных и количество символов. В этом случае печать может быть выполнена словом TYPE ( A1, LENGTH  ), которое снимает со стека упомянутые параметры и печатает соответствующие символы, начиная с текущей позиции курсора.

Для хранения строк используется понятие «строка со счетчиком», в которой первый байт (с нулевым смещением) хранит количество выводимых символов. Такая строка может быть представлена одним числом – адресом ячейки, хранящей значение счетчика. В этом случае выводимые символы хранятся, начиная со следующего адреса, а длину строки можно получить, прочитав байт по адресу строки. Преобразование адреса строки со счетчиком в формат, требуемый для слова TYPE, выполняется словом COUNT. Оно легко выражается через уже имеющиеся слова Форта.


: COUNT ( A --> A+1, [A] ) DUP 1+ SWAP C@ ;


Кроме того, внутри определений можно использовать уже упоминавшуюся конструкцию .” Текстовая информация“. Слово «точка-кавычка» компилирует код, при исполнении выводящий на экран строку вплоть до закрывающей кавычки. Символ кавычки может быть напечатан словом EMIT.

Похожее слово .( («точка-скобка») работает и в режиме компиляции, и в режиме исполнения.

Для упрощения работы с кавычками и пробелами (которые служат для разделения слов и потенциально могут являться источниками проблем при задании текста) в Форте определены две константы.


QUOTE – кладет на стек код кавычки.

BL – кладет на стек код пробела.


Кроме того, слово SPACE выводит пробел в текущую позицию курсора, а слово SPACES выводит N пробелов, снимая число N со стека.


Для задания строки, как уже упоминалось, требуется указать начальный адрес массива, содержащего символы, и его длину. Строка со счетчиком делает это автоматически, но ее недостатком является невозможность определения более 255 символов. Поэтому в Форте часто используют слово S” , которое задает строку сразу в формате (адрес, длина). Поскольку оба этих числа хранятся на стеке и представлены в целочисленном формате, строки теоретически могут занимать всю доступную память. Для работы с такими строками уже не требуется использовать слово COUNT. Например:


S” Это строка, которая может занимать очень много места в памяти” TYPE


Такой формат предпочтительнее для использования, он позволяет рассматривать строки как обычные массивы символов (и копировать их словами CMOVE и CMOVE>). Однако выбор конкретного формата представления строк оставляется целиком на усмотрение программиста. Форт имеет множество средств, помогающих осуществить перевод из одного формата в другой. Что особенно важно, приведение типов является не встроенной возможностью транслятора, а целиком подконтрольным процессом. Для строк можно использовать и ASCIIZ представление, и формат UU-Encode, и многое другое.

Для управления координатами вывода служат слова CR (выполняет перевод строки), и AT-XY ( X, Y  ), устанавливающее курсор в позицию с координатами, снимаемыми со стека.


Для ввода символов используется следующих набор слов.


Имя Стековая нотация Описание
KEY ( → CHAR ) Возвращает на стеке код нажатой клавиши.
?KEY ( → FLAG ) Возвращает на стеке ИСТИНУ, если была нажата какая-либо клавиша.
EXPECT ( A1, N → ) Принимает с клавиатуры строку не более чем из N символов, размещая их с адреса A1. Число реально введенных символов будет записано в переменную SPAN.
ACCEPT ( A1, N → +N ) Принимает с клавиатуры строку не более чем из N символов, размещая их с адреса A1. Число реально введенных символов будет оставлено на стеке.

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

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

7. Файловые операции

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

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

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

Стандартом предусмотрены следующие слова, обеспечивающие выполнение основных файловых операций.


Имя Стековая нотация Описание
CLOSE-FILE ( fileid → ior ) Закрывает файл, заданный на стеке идентификатором. Возвращает на стеке код результата операции, сообщенный операционной системой.
CREATE-FILE ( addr, u, fam → fileid, ior ) Создает и открывает файл, заданный адресом первого байта имени addr, длиной строки u и атрибутами fam (file access method). Возвращает на стеке идентификатор файла и код результата операции.
DELETE-FILE ( addr, u → ior ) Удаляет файл заданный адресом первого байта имени addr и длиной строки u. Возвращает на стеке код результата операции.
FILE-POSITION ( fileid → ud, ior ) Возвращает текущую позицию указателя чтения/записи в файле, заданном идентификатором, и код результата операции.
FILE-SIZE ( fileid → ud, ior ) Возвращает размер файла, заданного идентификатором, и код результата операции.
INCLUDE-FILE ( fileid → ) Загружает (интерпретирует) файл, заданный идентификатором. Побочные стековые эффекты зависят от содержимого интерпретируемого файла.
INCLUDED ( addr, u → ) Загружает (интерпретирует) файл, заданный адресом первого байта имени и длиной имени. Побочные стековые эффекты зависят от содержимого интерпретируемого файла.
OPEN-FILE ( addr, u, fam → fileid, ior ) Открывает существующий файл, заданный адресом первого байта имени addr, длиной строки u и атрибутами fam (file access method). Возвращает на стеке идентификатор файла и код результата операции.
READ-FILE ( addr, u1, fileid → u2, ior ) Читает не более u1 байт из файла, заданного идентификатором fileid, и размещает их начиная с адреса addr. Возвращает на стеке количество реально считанных байт u2 и код результата операции.
READ-LINE ( addr, u1, fileid → u2, flag, ior ) Читает следующую строку, но не более u1 байт, из файла, заданного идентификатором fileid, и размещает их начиная с адреса addr. Возвращает на стеке количество реально считанных байт u2, флаг flag и код результата операции. Флаг содержит логическую ИСТИНУ, если удалось прочитать очередную строку, и ЛОЖЬ в противном случае (например, если указатель чтения/записи находится в конце файла; в этом случае операционная система выдаст код успешного завершения операции, что не даст возможность распознать отсутствие новых строк для чтения).
REPOSITION-FILE ( ud, fileid → ior ) Устанавливает указатель чтения/записи файла, заданного идентификатором fileid, в позицию ud. Возвращает на стеке код результата операции.
RESIZE-FILE ( ud, fileid → ior ) Устанавливает размер файла, заданного идентификатором fileid, равный ud. Возвращает на стеке код результата операции.
WRITE-FILE ( addr, u, fileid → ior ) Записывает u байт, начиная с адреса addr, в файл, заданный идентификатором fileid. Возвращает на стеке код результата операции.
WRITE-LINE ( addr, u, fileid → ior ) То же, что и WRITE-FILE, но записываемые символы завершаются последовательностью, задающей перевод строки.

Для задания метода доступа к файлу можно пользоваться следующими константами.


Имя Значение
R/O Только чтение (read only).
R/W Чтение и запись (read/write).
W/O Только запись (write only).

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

Ранние стандарты языка Форт предусматривали также организацию информации в виде блоков размером в 1024 байта каждый. Поскольку размер блока кратен размеру одного сектора магнитного диска, появлялась возможность обращаться к диску напрямую, минуя функции операционной системы. Таким образом, Форт, использующий блочную организацию файлов, может быть запущен вообще без операционной системы. Описание такого подхода можно найти в стандарте (набор слов BLOCK), и в литературе []. Для мощных аппаратных платформ данная возможность Форта уже не вполне актуальна, но она может быть использована в т.н. встраиваемых системах, выполняемых на базе простых микроконтроллеров и микроЭВМ. Разработка программ блочного доступа к данным в таких системах достаточно специфична и не входит в круг рассматриваемых здесь вопросов.

8. Поиск слов. Контекстные словари. Сравнение контекстных словарей и векторных слов

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

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

Упрощенно схему поиска можно представить так: по строке, представляющей собой имя слова, Форт находит адрес, который содержит информацию о действии слова. Это адрес иначе называется CFA (Code Field Address – адрес поля кода). В простейшем случае на этот адрес нужно просто передать управление.

(…)


Форт предоставляет две альтернативные возможности для изменения действия слова с определенным именем. Можно определить аналогичное слово в другом контекстном словаре, или же определить одно векторное слово, которое затем будет указывать на требуемую версию. В чем преимущества и недостатки этих подходов? На первый взгляд, оба этих приема служат одной и той же цели – обеспечить выполнение различных действий словами с одним именем. Различие между ними кроется в особенностях реализации того и другого подхода. Пользуясь терминологией объектно-ориентированного программирования, вкратце можно сказать, что словари обеспечивают статическое связывание, а векторные слова – динамическое. Это означает, что определив AND как слово в другом контекстном словаре, впоследствии необходимо будет явно указывать, какой именно версией этого слова должен воспользоваться Форт. Это является преимуществом, если действительно необходим жесткий контроль над действием слова с каким-либо именем. Кроме AND есть похожие слова OR и XOR, а также BL, которое в ассемблере является именем регистра, а в Форте – кодом пробела (константой 32). Все эти слова удобнее определить в новом контекстном словаре, а затем, переключая словари, определить, что именно требуется сделать – указать на регистр, или положить на стек код пробела.

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

9. Практические приемы программирования на Форте

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

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

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

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

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

Положение изменилось с появлением программ, управляемых событиями. Этот подход реализован в широко известном графическом интерфейсе операционной системы Windows. Здесь пользователь избавлен от необходимости «спускаться» по вложенным текстовым меню, а может сразу указать мышью на нужный ему объект или кнопку, использовать горячие клавиши для выбора пункта меню и т.д. Аварийные ситуации обрабатываются самой операционной системой, возвращающей результат выполнения операции. В случае неудачи пользователь получит на экране диалоговое окно, которое сможет закрыть, чтобы вернуться в основную программу и попытаться выполнить какое-то другое действие. Поскольку жесткого порядка перехода между различными подпрограммами здесь нет, программирование такого интерфейса сводится к описанию реакции на активизацию того или иного элемента управления, находящегося в окне программы. Средства разработки класса RAD (Rapid Application Development), такие как Delphi и C Builder, доводят этот принцип до логического завершения, полностью освобождая разработчика от необходимости заниматься программированием проверок выбора нужной кнопки, пункта меню, и т.п. Нужные шаблоны генерируются автоматически, и остается подставить в них необходимые действия.

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

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

9.1. Общие рекомендации по форматированию исходного текста программ

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


Если определение нового слова занимает более одной строки, его желательно отформатировать в определенном стиле. А именно:


: NEW-WORD ( стек до исполнения --> стек после исполнения )

   Слово1

      Другой логический уровень слов.

      СЛОВА ОДНОЙ ГРУППЫ ДРУГАЯ ГРУППА СЛОВ

      ( Комментарий ) СЛОВО СЛОВО

      \ Строка комментариев, в ней можно использовать символ ")"

;


: ЕЩЕ_СЛОВО ( обратите внимание на пустую строку между двумя определе­ниями )


После двоеточия и имени слова крайне желательно привести стековую нотацию в круглых скобках. Круглые скобки ограничивают комментарии (не забывайте ставить хотя бы один пробел после открывающей скобки). Слева от стрелки указывается состояние стека до вызова слова, справа от стрелки - то, что будет после исполнения слова. Если состояние стека после исполнения зависит от исходного состояния, разделите варианты символом | Например: ?DUP ( A --> A, A| A ). Стек вещественных чисел описывается комбинацией F: FDUP ( F: X --> F: X, X ). Желательно, что­бы имена в стековой нотации давали некоторое представление о назначе­нии числа: ADR, COUNT, Handle, Length и т.п. Идентичные числа (напри­мер, получаемые словом DUP) обозначаются одним и тем же иденти­фикатором.

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

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

При наборе чисел внутри определений убедитесь, что установлена необходимая система счисления. Следует различать систему счисления в момент компиляции определения и в момент исполнения. Например, при компиляции


HEX : WORD1 3F ;


слово WORD1 будет скомпилировано таким образом, что в момент исполне­ния на стек будет положено число 63 (3FH). Происходит это потому, что перед началом компиляции определения Форт был переключен в шестнадца­теричную систему. Исполнение


: WORD1 HEX 3F ;


не приведет к желаемому результату, если только перед началом опреде­ления шестнадцатеричная система уже не была установлена. Однако при исполнении WORD1 Форт переключится в шестнадцатеричную систему. Необ­ходимо взять за правило устанавливать требуемую систему счисления пе­ред началом определения новых слов:


DECIMAL

Слова, использующие десятичную систему счисления.

HEX

Слова, использующие шестнадцатеричную систему счисления.


Переключение системы счисления можно выполнить и внутри определе­ния, используя слова [ и ]


: WORD1 [ HEX ] 3F [ DECIMAL ] ;


Слово [ переключает Форт в режим исполнения, заставляя исполнить все слова до слова ] , которое опять устанавливает режим компиляции. После компиляции числа 3F слово DECIMAL опять устанавливает десятичную систему.

Временное переключение в режим исполнения бывает полезно и для вычисления некоторых констант. Например [ 2 2 * ] LITERAL внутри опре­деления равносильно использованию числа 4. Вычисленное в режиме испол­нения число 4 остается на стеке и должно быть скомпилировано в код словом LITERAL.

9.2. Работа с массивами

Создание массива:


CREATE ARRAY[] 1000 ALLOT


Создается массив с именем ARRAY[] и выделяет в памяти 1000 байт. Квадратные скобки после имени необязательны и не обязательно означают, что имя является именем массива. В действительности Форт не делает различий между именами в зависимости от присутствия в них специальных символов. Однако рекомендуется придерживаться некоторого стиля в выбо­ре имен для переменных, массивов и тому подобных слов, с тем, чтобы программист сам мог хотя бы приблизительно ориентироваться в назначе­нии слова по его имени. Слово ARRAY[] при использовании имеет следую­щее действие: кладет на стек адрес вершины сегмента данных на момент создания слова. Строка 1000 ALLOT перемещает указатель свободной памя­ти на 1000 байт, таким образом, область размером 1000 байт оказывается свободной, а ее начальный адрес кладется на стек словом ARRAY[]

Форт не делает различий между массивами, структурами и просто пе­ременными. Например, слово VARIABLE определяется в Форте следующим об­разом: CREATE 4 ALLOT. Как можно видеть, разница только в размере ре­зервируемой памяти.


Доступ к элементу массива.


: GET-ELEMENT ( INDEX --> DATA )

   4 * ARRAY[] + @

;


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

10 GET-ELEMENT


Это слово ожидает получить на стеке номер элемента массива, счи­тая от нуля. Этот номер умножается на 4 ( 4 * , обратите внимание на постфиксную запись этой формулы), затем к нему прибавляется начальный адрес массива: ARRAY[] + . В результате мы имеем на стеке абсолютный адрес в сегменте данных требуемого элемента. Слово @ кладет вместо него на стек содержимое памяти по этому адресу. Приведенный пример относится к 4-байтным переменным. Если необходим другой размер и/или тип переменной (например, вещественная переменная размером 8 байт), изме­ните число, на которое умножается номер элемента (это число равно раз­меру одного элемента) и слово, выполняющее разыменование: C@ W@ F@ и т.п. Учтите, что при выходе индекса за границу массива Вы не получите сообщение об ошибке, а вместо этого на стек будет положено содержимое памяти по соответствующему адресу. При слишком большом индексе есть вероятность вообще выйти за предел сегмента данных.


Обнуление массива.


: CLEAR-ARRAY

   250 0 DO

      0 I 4 * ARRAY[] + !

   LOOP

;


Поскольку в 1000 байт умещается 250 чисел, мы организуем цикл от 0 до 249 включительно. 0 в теле цикла - это заготовка для слова ! Строка I 4 * ARRAY[] + вычисляет адрес элемента с номером I (а I явля­ется счетчиком цикла и изменяется от 0 до 249). Перед выполнением сло­ва ! на стеке лежит 0, а поверх него адрес I-го элемента. Таким обра­зом, ! записывает 0 по адресу I-го элемента (см. описание этого слова). При необходимости обнуления (или занесения начального значения) в массив, содержащий числа другого типа рекомендации такие же, как и для предыдущего примера. Учтите также, что при работе с вещественными чис­лами вместо 0 необходимо использовать запись 0.0 и F! вместо !, пос­кольку целочисленный и вещественный стеки различаются и автоматического преобразования чисел не происходит.


Вычисление суммы элементов массива.


: SUM ( --> SUM )

   0

   250 0 DO

      I 4 * ARRAY[] + @ +

   LOOP

;


Перед выполнением основного цикла на стек кладется 0, к которому будут прибавляться остальные элементы. Дальнейшее накопление суммы происходит в основном цикле.

Строка до слова @ включительно уже знакома - она кладет на стек элемент с номером I. Слово + (плюс) после него добавляет этот элемент к предварительно положенному на стек нулю. Повторение этой операции для всех элементов массива последовательно увеличивает сумму. В ре­зультате после выхода из цикла вместо нуля на стеке будет находиться сумма всех элементов массива.

Организация вычислений с вещественными числами

Способы представления вещественных чисел и арифметические операции

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

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

Формат с фиксированной точкой весьма прост для реализации на процессорах с целочисленной системой команд и достаточно эффективен для ряда применений. Для представления некоторого числа в этом формате достаточно умножить его на заранее определенную константу (удобнее всего на целую степень двойки – сдвинув на несколько разрядов влево). Например, если мы выбираем в качестве такой константы 1000, то число 3,54 будет записано в виде 3,54*1000 = 3540. На самом деле это целочисленная запись, но вместо количества единиц указывается количество тысячных долей. Для получения истинного значения числа оказывается достаточно поставить десятичную точку в некоторую фиксированную (в нашем случае – отделив точкой три младших разряда) позицию, откуда и появилось название формата.

На практике под дробную часть числа обычно отводят фиксированное количество двоичных разрядов. Например, в 16-разрядном числе первые 8 разрядов могут представлять целую часть числа, а остальные – дробную часть. Такой формат обозначается как 8:8, где перед двоеточием указывается количество двоичных разрядов для целой части, а после двоеточия – для дробной части числа. В зависимости от решаемой задачи можно использовать различное количество разрядов как для целой, так и для дробной части. Например, возможны форматы 12:4, 16:16 или даже 32:32. В последнем случае целая и дробная части хранятся в отдельных 32-разрядных ячейках памяти.

Правила выполнения арифметических действий над числами с фиксированной точкой достаточно просты. Сложение и вычитание выполняется точно так же, как и для целых чисел. Например:


00000001,10000000+00000010,01000000=00000011,11000000


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


1,5+2,25=3,75


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

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


10010001 = 1·27+0·26+0·25+1·24+0·23+0·22+0·21+1·20,


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


0,10000000=1·2-1+0·2-2+0·2-3+0·2-4+0·2-5+0·2-6+0·2-7+0·2-8


Таким образом, 0,10000000 представляет десятичное число 0,5, а 0,11000000 – число 0,75 (0,5+0,25).


Умножение чисел с фиксированной точкой отличается тем, что положение точки в результате смещается: например, умножение двух чисел формата 8:8 дает результат в формате 16:16. Это является следствием правила умножения в столбик: число знаков после точки в обоих перемножаемых числах суммируется для получения числа знаков после точки результата.

Похожая ситуация наблюдается и для деления – 16:16/8:8 = 8:8.

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

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

Для решения этой проблемы используется формат с плавающей точкой. Как следует из названия, десятичная точка не закреплена в фиксированной позиции, а может подставляться в произвольное место. Смещение десятичной точки от начального положения называется порядком числа, а само представление числа без учета положения точки – мантиссой. Таким образом, двоичное число 0100,10012 может быть представлено в виде 010010012 (мантисса) и –4 (порядок). Значение –4 показывает, что для получения правильного значения представленного числа следует перенести десятичную точку на 4 разряда влево.

Значение порядка специально записано в десятичном виде. Поскольку порядок может быть как положительным, так и отрицательным (т.е. десятичная точка переносится как влево, так и вправо), для его записи необходимо использовать число со знаком. Это несколько усложняет преобразования чисел с плавающей точкой, поэтому вместо числа со знаком используется обычное положительное число, но с одним замечанием: отсутствию смещения десятичной точки соответствует некоторое положительное число. Обычно это половина от возможного максимального значения порядка. Например, если для порядка выделено 8-разрядное двоичное число с возможными значениями от 0 до 255, то можно условиться, что значение порядка 128 соответствует отсутствию смещения десятичной точки. Тогда смещение на 4 разряда влево будет представлено порядком 128-4=124, или 011111002.

С использованием всего 8 двоичных разрядов оказывается возможным смещение десятичной точки на 128 разрядов влево или на 127 разрядов вправо. Таким образом, эквивалентное по представляемому диапазону число с фиксированной точкой должно иметь по меньшей мере 256 двоичных разрядов. Формат с плавающей точкой оказывается значительно экономнее.

Одно и то же число может быть записано в формате с плавающей точкой различными способами. Следующие записи будут эквивалентны:


00001,111·21, 0000,1111·22, 000,01111·23.


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

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

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


0,11011011·21+0,10010110·23


Поскольку первое число имеет меньший порядок, его необходимо денормализовать. Эквивалентной записью для него будет 0,00110110·23

Теперь можно выполнить сложение мантисс: 0,001101102+0,100101102=0,110011002

Порядок результата равен 3.

Таким образом, результатом сложения является 0,110011002·23


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


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

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

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

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

Трансцендентные функции

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

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

Например, если существует способ вычисления функции , то можно использовать известные из тригонометрии соотношения:

Исторически трансцендентные функции появились как результат решения дифференциального уравнения второго порядка. Развитие механики привело к необходимости описания движения тел в поле тяготения Земли (свободного падения и механических колебаний). Поскольку закон всемирного тяготения позволяет определить ускорение, т.е. вторую производную от координаты, уравнение движения в общем случае будет представлять собой дифференциальное уравнение второго порядка. Если это уравнение имеет больше одного ненулевого члена, то его решением будет являться трансцендентная функция. Существует две функции, удовлетворяющих такому уравнению – sin(x) и ex. Обе эти функции бесконечно дифференцируемы и не могут быть получены путем обычных арифметических вычислений.

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

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

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

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

Этот ряд использует некоторую опорную точку x0 для вычисления функции в ее окрестностях. Производные в данной точке имеют фиксированные значения, так что искомая функция оказывается рядом, разложенным по целым степеням величины (x-x0). Зная величину (x-x0)n, можно получить (x-x0)n+1 простым умножением.

Основные трансцендентные функции имеют следующее разложение:


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

Радиус сходимости следующих рядов ограничен и равен 1.

(формула также называется биномом Ньютона).

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

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

На практике достаточно ограничиться первыми N членами ряда. Число N может быть фиксировано, а может выбираться таким образом, чтобы значение N+1-го члена ряда не превышало некоторой величины. Альтернативным вариантом является оценка по относительной ошибке – величина следующего члена ряда, отнесенная к уже накопленной сумме, должна быть меньше заранее заданного предела. Последний вариант более предпочтителен, так как позволяет обеспечить точное заполнение мантиссы результата при различных значениях аргумента.

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

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

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

Форматные преобразования

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

Для формата с фиксированной точкой достаточно воспользоваться тем фактом, что целая часть представляемого числа находится в его старших разрядах. Это позволяет достаточно просто переходить от целочисленного формата к формату с фиксированной точкой и обратно.

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

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

1) Производится нормализация десятичного представления числа (мантисса приводится к диапазону 0..1). Одновременно определяется десятичный порядок как количество шагов умножения/деления мантиссы до получения требуемого диапазона.

2) Очередная цифра определяется после умножения мантиссы на 10.

3) Целая часть от деления является очередной цифрой для печати.

4) С дробной частью повторяются шаги 2-3 до вывода требуемого количества знаков мантиссы.

5) Печатается порядок. Если число было выведено в формате с мантиссой, лежащей в диапазоне 1..10, порядок уменьшается на единицу.


Пример:


Число 12.23Е1 = 122.3

1) 122.3/1000 = 0.1223 Порядок равен 3.

2) 0.1223*10=1.223

3) Целая часть равна 1.

4) Дробная часть равна 0.223.

5) Повторяем шаг 2: 0.223*10=2.23 и т.д.


Результат вывода: 1.22300000000Е2.


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


Алгоритм преобразования строки в вещественное число:

1) Определяется знак и исключается из строки.

2) Если очередной символ является цифрой, то он обрабатывается как: F=F+N10-i, где F – накопленная мантисса; i - номер обрабатываемой цифры, начиная с нуля; N – десятичный эквивалент этой цифры. Знак мантиссы и десятичная точка не изменяют номер обрабатываемой цифры.

3) Если очередной символ является десятичной точкой, то запоминается количество уже обработанных цифр для выполнения коррекции по п.5.

4) Если очередной символ является символом порядка, дальнейшая строка обрабатывается как целое число, представляющая собой десятичный порядок.

5) Выполняется коррекция порядка в соответствии с позицией точки в строке. Это необходимо для правильного преобразования строк, имеющих более одной цифры перед десятичной точкой, например, 12345.3Е3.

6) Мантисса домножается на 10p, где p – скорректированный порядок.


Пример:


Строка 12.435E-3

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

Первый символ является цифрой. Текущий номер обрабатываемой цифры равен нулю, поэтому формируется мантисса, равная 1·100.

Второй символ также является цифрой. К мантиссе добавляется величина 2·10-1.

Третий символ является точкой. Запоминается текущее количество обработанных цифр для последующей коррекции порядка.

Аналогичные операции проводятся с остальными символами вплоть до символа E.

Строка после символа E обрабатывается как целое число со значением –3.

Производится коррекция порядка: к числу –3 добавляется количество обработанных цифр до десятичной точки. –3+(–1) = –4.

Результатом преобразования будет являться число 1.2435E–4.


Оглавление

  • 1. Основные сведения о языке
  • 2. Работа со стеком и арифметические операции
  • 3. Структуры управления
  • 4. Создающие определения
  • 5. Переменные
  • 6. Ввод-вывод. Текстовый вывод на экран, ввод с клавиатуры
  • 7. Файловые операции
  • 8. Поиск слов. Контекстные словари. Сравнение контекстных словарей и векторных слов
  • 9. Практические приемы программирования на Форте
  •   9.1. Общие рекомендации по форматированию исходного текста программ
  •   9.2. Работа с массивами
  • Организация вычислений с вещественными числами
  •   Способы представления вещественных чисел и арифметические операции
  •   Трансцендентные функции
  •   Форматные преобразования