"Хорошая программа - документированная программа" Надпись на памятнике Недокументированной Программе. Главный аргумент противников Форта (который, к сожалению, часто бывает справедливым) - "а почему Форт работает только с целыми числами?". Естественно, бывают задачи, для которых целых чисел вполне достаточно. Но если все-таки возникла потребность в использовании плавающей точки, то Форт, на первый взгляд, можно свернуть в архив, перебросить на дискету и положить ее поглубже в ящик, а потом занять пару десятков мегабайт на винчестере под что-нибудь объектно-ориентированное. Однако одним из основных принципов Форта является отсутс- твие запретов на что-либо. Следовательно, операции с плавающей точкой также не запрещаются (Форту просто все равно, что он де- лает с числами - ADD или FADD). Правда, констатировать возмож- ность введения таких операций - это одно, а реализовать их на практике - совершенно другое... Тем не менее, такая реализация была произведена мной в течение нескольких месяцев, начиная с апреля 1996 года (можно было сделать и за пару недель, но я за- нимался не только Фортом, а на Форте - не только плавающей точ- кой). Для такой надстройки над Фортом был выбран СП-Форт версии 2.02 А.Ю. Черезова. К тому есть несколько причин: 1) Поставляется с исходными текстами. 2) Модель с раздельными сегментами. 3) Машинный код. 4) При написании имелся в виду именно Форт, а не Паскаль или С++ с синтаксисом Форта (не буду уточнять, чтобы не обидеть, но в некоторых версиях Форта есть слово, создающее окно на экра- не, которое при этом снимает со стека порядка десяти параметров - не проще было вместо Форта заняться библиотечками под С++?). 5) Freeware. 6) Наконец, в этой версии Форта не хватает только плавающей точки. Все остальное (за редким исключением) там есть. По край- ней мере, есть возможность сделать, если необходимо. (см. п.1) Итак, предлагаемая надстройка обеспечивает возможность ис- пользования в Форте чисел с плавающей точкой. Правда, к машине предъявляется одно требование - у нее должен быть сопроцессор! А еще лучше, чтобы это был сопроцессор 80387 и выше. Причина этого требования очень проста - в качестве стека для чисел с плавающей точкой используется стек сопроцессора. Операции с этими числами выполняются также сопроцессором. Эмуляция этих вычислений при отсутствии сопроцессора пока отсутствует. Здесь крайне уместно было бы привести немного теории. Стек сопроцессора представляет из себя восемь регистров. Каждый ре- гистр имеет размер 10 байт, имеет тэг занятости, и может хранить 1 (одно) число с плавающей точкой в так называемом временном ве- щественном формате. Вообще же форматов у сопроцессора несколько, а приведены они в таблице: Таблица 1 Название Длина, байт Диапазон Как это было бы (примерно) в С++ Целое слово 2 -32768..32767 short Короткое целое 4 -10**9..10**9 long Длинное целое 8 -10**18..10**18 ? Двоично-десятичное 10 -10**18..10**18 Внутренний Короткое вещественное 4 -10**38..10**38 float Длинное вещественное 8 -10**308..10**308 double Временное вещественное 10 -10**4932..10**4932 long double Вообще сопроцессор работает только с последним из приведен- ных форматов. Остальные он использует только при записи чисел в память и загрузке их оттуда. При этом программист должен явно указать, в какой именно формат нужно преобразовать число. На ас- семблере это делается так: Таблица 2 Название Загрузка Сохранение Целое слово WORD [BX] FILD WORD [BX] FISTP Короткое целое DWORD [BX] FILD DWORD [BX] FISTP Длинное целое QWORD [BX] FILD QWORD [BX] FISTP Короткое вещественное DWORD [BX] FLD DWORD [BX] FSTP Длинное вещественное QWORD [BX] FLD QWORD [BX] FSTP Временное вещественное TBYTE [BX] FLD TBYTE [BX] FSTP Перед выполнением команды DS:[BX] указывает на первый байт числа. В данном варианте после сохранения происходит выборка числа со стека сопроцессора. Чтобы этого не происходило, необхо- димо использовать команды FIST и FST. В предлагаемой библиотеке в качестве основного выбрано длинное вещественное. При выполнении преобразований обычно происходит потеря точ- ности. В этом случае получаемый результат округляется сопроцес- сором по одному из четырех правил: 1) Округление вверх. Результат округляется к плюс бесконеч- ности. 2) Округление вниз. К минус бесконечности. 3) Округление к нулю. Самый полезный режим (на мой взгляд). То, что не влезает в новый формат, просто отбрасывается. 4) Округление к ближайшему. В принципе, это должно бы рабо- тать как обычное математическое округление, т.е. 1.5=2, 1.49=1. Однако так происходит не всегда, потому что полное название это- го режима - "округление к ближайшему или четному". Для иллюстра- ции можно попробовать брать десятичные логарифмы от разных чисел и округлять их (F[LOG10] в данной библиотеке). Казалось бы, lg1=0, lg10=1, lg100=2, lg1000=3 и т.д. Но из-за "округления к четному" это не выполняется. Главные трудности возникают при пе- чати чисел - их порядок определяется неправильно. Режим округления задается битами 10 и 11 в двухбайтном ре- гистре, который называется "управляющее слово сопроцессора" (FPU Control Word). Там есть и разные всякие другие биты, но пока я практического применения им не нашел, поэтому не буду описывать то, что при желании можно найти в справочнике. Итак: Таблица 3 бит 10 бит 11 Режим 0 0 К ближайшему или четному 0 1 Вниз 1 0 Вверх 1 1 К нулю В сопроцессоре есть еще один двухбайтный регистр, называе- мый "слово состояния сопроцессора" (FPU State Word). В нем, кро- ме всего прочего, хранятся аналоги флажков нуля, переноса, знака и т.п. Путем некоторых манипуляций эти флажки могут быть скопи- рованы в обычные флажки процессора, и тогда можно делать услов- ный переход по результату проверки стека сопроцессора на ноль, перенос и т.д. Как это делать, можно посмотреть в float.f Хватит пока теории, нужно и описать то, что позволяет де- лать Форт после загрузки float.f. Единственным недостатком СП-Форта, мешающим введению обработки вещественных чисел, явля- ется отсутствие виртуального слова NUMBER. Оно описано в файле kernel6.f и его необходимо чуть-чуть подправить. Вот что было сделано: 1) Перед определением слова NUMBER добавлена строка VECT NUMBER 2) NUMBER переименовано в SNUMBER. Еще можно было переиме- новать в NUMBER1 и т.д., но в данном случае S обозначает data Stack. 3) Добавлена строка ' SNUMBER TO NUMBER Все. Больше ничего делать не надо (кроме перекомпиляции Форта и ассемблера). Все остальное берет на себя float.f. Во-первых, виртуальный NUMBER указывает теперь на FNUMBER, опи- санный в float.f. Во-вторых, ?LITERAL, который, к счастью, ока- зался виртуальным, тоже переопределяется, и вещественные числа можно использовать внутри определений (без этого все возможности Форта свелись бы к обычному калькулятору). Возможные форматы чисел: -1.2345Е0 1.2345Е11 1Е-11 0.00002Е-001 Согласно требованиям стандарта 94-года (предоставленного мне А.Ю. Черезовым, за что ему оооооооогромное спасибо), опреде- ляющим признаком "плавательности" числа является наличие буквы Е. В существующей реализации именно по этой букве и ориентирует- ся FNUMBER в своей попытке определить, должен ли он преобразо- вать это число сам, или передать его дальше. Маленький мой не- догляд, который я пока не стал исправлять - буква должна быть латинская (ну это везде так) и заглавная (а вот это уже не вез- де). Тем не менее, хотя так и не было задумано, я все же решил не бросаться исправлять такое положение вещей. Дело в том, что стандарт предусматривает, что Форт должен страшно ругаться, если вдруг кому-то придет в голову поработать с вещественными числа- ми, если система счисления не десятичная. На мой взгляд, это крайне спорное утверждение. Ведь если я сначала делаю, скажем HEX .......... 0305 INPORT \ вполне реальный пример, я именно так и делал \ вчера DUP . S>F 10.43E6 F* F. то скажите, пожалуйста, почему я должен делать еще и DECI- MAL где-то между INPORT и 10.43Е6 ? Ведь я в случае целочислен- ных чисел имею в виду адрес порта, который удобнее задавать шестнадцатиричным, а потом то, что я оттуда читаю, мне нужно ум- ножить на частоту генератора, которая в целочисленный формат ле- зет, мягко говоря, с трудом. Естественно, я делаю это на стеке сопроцессора, где шестнадцатиричная система счисления просто не имеет смысла ( как бы Вам понравилось увидеть F.12AB56E-2 в ка- честве результата?). Тем не менее, 10.43Е6 - вполне нормальное целое число (если забыть про вещественные и вспомнить, что Е=14 в десятичной системе, а точка в Форте - признак того, что число занимает четыре байта). Целочисленный NUMBER в шестнадцатиричной системе положил бы на стек четыре байта без раздумий (вернее, с раздумьями порядка сотен микросекунд). Но если работает FNUMBER, то строка 10.46Е6 просто до целочисленного NUMBERа не дойдет - число будет воспринято как вещественное. Именно по этому принци- пу работает float.f. Но вот если система счисления все-таки шестнадцатеричная (или хотя бы пятнадцатиричная - чтобы Е было цифрой), то обычное целое число 1Е3 будет воспринято как 1000. И все из-за буквы Е ! Выход - набрать 1е3. Тогда FNUMBER не найдет в строке символа с кодом, равным коду E и число спокойно проплы- вет на стек данных. Но все это очень нестандартно и описано только как способ облегчить себе жизнь. Скорее всего, следующая версия будет гораздо больше соответствовать стандарту. Итак, FNUMBER здесь есть. Кроме него есть еще FLITERAL, ко- торый занимается примерно тем же, но в режиме компиляции. Для целых чисел достаточно скомпилировать инструкции AX, NN MOV и AX PUSH , где NN - число, любезно предоставленное целочисленным NUMBERом. Таким образом, если мы используем целое число внутри определения, то оно как бы хранится в сегменте кода в виде аргу- мента при непосредственной адресации. С сопроцессором так сде- лать нельзя, поскольку он должен загрузить на свой стек число из DS:[BX]. Поэтому FLITERAL делает следующее: 1) Число со стека сопроцессора (его туда поместил FNUMBER) переносится в HERE. 2) Компилируется инструкция MOV AX, NN, где NN = HERE. 3) Компилируется обращение к FLIT, которое копирует AX в BX и делает QWORD [BX] FLD. 4) В сегменте данных резервируется 8 байт, чтобы обойти скомпилированное туда число с плавающей точкой. Примечание: чтобы скомпилировать не MOV AX, NN и MOV BX, AX, а сразу MOV BX, NN, надо хотя бы иметь такую возможность в СП-Форте. Там есть все регистры общего назначения, кроме BX. Можно, конечно, и это исправить, но тогда придется еще и ker- nel8.f ломать... Главное в этом то, что вещественнное число, используемое внутри определения, хранится уже в сегменте данных. Не вижу в этом ничего принципиально неприемлемого. Нужно еще учесть, что сегмент данных растет в СП-Форте гораздо медленнее сегмента ко- да, а вещественное число занимает 8 байт вместо 2, так что в медленно растущем сегменте ему самое место. Теперь, наконец, опишем все слова, добавляемые float.f (в порядке описания в файле) FLOAT - создает вещественную переменную; пример - FLOAT X FDUP - дублирует вершину вещественного стека FDROP - удаляет число с вершины вещественного стека FSWAP - меняет местами два верхних числа на вещественном стеке S>F - переносит число со стека данных на вещественный стек F>S - переносит число с вещественного стека на стек данных, округляя его; округление выполняется сопроцессором и зависит от установленного режима (см. выше) F! ( ADR -> ) - снимает с вещественного стека число и запи- сывает его по адресу, который снимает со стека данных F@ ( ADR -> ) - снимает со стека данных адрес и загружает на вещественный стек число, находящееся по нему GETFPUCW - скопировать управляющее слово сопроцессора в пе- ременную FPUCW SETFPUCW - загрузить управляющее слово сопроцессора из пе- ременной FPUCW TRUNC-MODE - установить режим округления "к нулю" ROUND-MODE - установить режим округления "к ближайшему" F+ - сложить два верхних числа на вещественном стеке F1+ - прибавить 1 к числу на вершине вещественного стека F- - вычесть число на вершине вещественного стека из числа, находящегося под ним F* - перемножить два верхних числа на вещественном стеке F/ - разделить второе сверху число на вещественном стеке на число на вершине FLOG10 - взять десятичный логарифм от числа на вершине ве- щественного стека F[LOG10] - то же, но с округлением (используется при печати и преобразовании чисел) F|| - второй вариант "FABS" - модуль числа на вершине F= - снимает два числа с вещественного стека и кладет на стек данных (!!!) TRUE, если они равны, или FALSE в противном случае F< - "меньше" для вещественного стека F> - "больше" для вещественного стека SIN - синус (только для 80387 и выше!!!) COS - косинус (только для 80387 и выше!!!) FSQRT - квадратный корень из числа на вершине вещественного стека F10X - со стека данных (!!!) снимается число, а на вещест- венный стек кладется 10 в этой степени (использует- ся при печати и преобразовании чисел) F. - снимает с вещественного стека число и печатает его POSITION ( A,N,C -> P ) - ищет символ C в строке, заданной адресом первого байта и длиной и кладет его пози- цию или N+1, если символ не найден (используется при преобразовании чисел) COMMAPOS - ищет позицию точки в строке (используется при преобразовании чисел) EXP-POS - ищет позицию буквы E в строке (используется при преобразовании чисел) SKIP1 ( А -> А ) - удаляет из строки первый символ (исполь- зуется при преобразовании чисел) >FLOAT ( А -> ) - пытается преобразовать строку со счетчи- ком в число с плавающей точкой (если не получается, ругается самостоятельно) FLOAT? ( A -> A, T/F ) - кладет поверх адреса TRUE, если строка со счетчиком по этому адресу может быть числом с плавающей точкой и система счисления десятичная FNUMBER - собственно то, ради чего все и затевалось FLIT ( ADR -> ) - загружает на стек сопроцессора число с адреса ADR FLIT, - компилирует вызов FLIT FLITERAL - компилирует число с плавающей точкой ?FLITERAL - новый вариант LITERALа, работающий еще и с ве- щественными числами F, - переносит число с вещественного стека в сегмент данных FCONSTANT - создает константу с плавающей точкой PI - кладет на вещественный стек 3.141592 и т.д. При печати вещественных чисел количество выводимых цифр на- ходится в переменной FFORM (обычная, а не QUAN). По умолчанию 12. Печать чисел идет через EMIT, поэтому ее также можно пере- направлять в файл, на принтер и т.д. Кроме FFORM есть еще две переменные - FPUCW и FPUSW, которые используются для копирования в них управляющего слова и слова состояния (их можно копировать только в память). Нужно отметить, что алгоритм печати веществен- ных чисел отличается от печати целых. В последнем случае строка для печати формируется начиная с младшего разряда и очередная цифра получается как остаток от деления числа на основание сис- темы счисления. С вещественными числами так сделать невозможно - остатка у них нет. Ввиду этого выделение очередной цифры проис- ходит следующим образом: 1) Определяется порядок числа (для этого и нужен F[LOG10]). 2) 10 возводится в степень порядка и помещается на вещест- венный стек. 3) Число делится на 10 в степени порядка. 4) Целая часть от деления и является очередной цифрой для печати (печатается немедленно). 5) Цифра умножается на десять в степени порядка числа и вы- читается из числа. 6) Все повторяется FFORM @ раз. 7) Печатается порядок после буквы E. Пример: Число 12.23Е1 = 122.3 1) Порядок равен 2. 2) 10 в степени 2 равно 100 3) 122.3/100 = 1 4) 1 печатаем 5) 122.3-1*100 = 22.3 6) 22.3 - новое число, с которым повторяем то же самое. Поскольку порядок равен 2, печатаем 2 (будет напечатано 1.22300000000Е2). Как и в большинстве языков, использующих вещественные чис- ла, иногда печатается внушительное количество девяток после нес- кольких "нормальных" цифр. Как с этим бороться, не знаю. По крайней мере, знаю анекдот о том, что 2+2=4, а если есть сопро- цессор, то 3.999999999999999999999 ... Здесь же стоит привести алгоритм преобразования строки в вещественное число. 1) Определяется знак и исключается из строки. 2) От начала строки и до точки (если есть, если нет - до символа Е) очередная цифра обрабатывается как: F=F+N*10**(-i), где i - позиция в строке обрабатываемой цифры, N - значение этой цифры, получаемое по DIGIT. 3) От точки до символа Е - то же самое, но степень не -i, а -i+1, чтобы учесть наличие точки в строке. 4) После буквы Е подразумевается порядок, который обрабаты- вается самостоятельно чем-то похожим на целочисленный NUMBER. 5) Выполняется коррекция порядка в соответствии с позицией точки в строке. Благодаря этому можно вводить 12345.3Е3, а не только 1.23453Е7, то есть перед точкой может быть любое коли- чество цифр. Чуть не забыл самое главное - размер стека. Он равен 8 (поскольку 8 регистров). Однако реально можно использовать 6, т.к. 2 использует >FLOAT. Печать тоже использует 2 регистра, по- этому есть опасность потерять данные. Тем не менее если печать пока не предвидится, стек можно использовать "на всю катушку". Кстати говоря, это тоже соответствует стандарту, который предпи- сывает выделять для вещественного стека at least 6 ячеек. Вполне возможно, что имелся в виду именно сопроцессор. Несколько слов о том, почему желателен 80387 и выше. Во-первых, этот самый 80387 и выше спроектирован в соответствии со стандартом IEEE-754. Но это не так важно. Важно то, что SIN и COS основаны на командах, которых в 80287 просто нет - там есть тангенс, а синуса с косинусом вот нет. Кстати говоря, ради этих двух команд был изменен asm.f. Собственно говоря, их можно прос- то стереть, если уж требуется совместимость со всеми-всеми-всеми сопроцессорами. И последнее. Данная реализация библиотеки поддержки плаваю- щей точки не является окончательной. Собственно говоря, float.f - первая нормально работающая реализация. Ввиду этого в ней воз- можны ошибки, неточности и т.д. Что мог - отловил и исправил. Исправления, естественно, будут продолжаться, как и добавление новых слов (сейчас система команд сопроцессора используется да- леко не полностью). Надеюсь, что моя работа кому-то поможет. 3.08.96 И.Е. Тарасов