![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
В статье приводятся работы, выполненные небольшим коллективом программистов на протяжении ряда лет. Сложилось так, что еще в 1987 году было принято решение разобраться в единственном доступном на тот момент компиляторе для персонального компьютера с языка PL/1, созданным американским специалистом Гарри Килдэллом (Gary Kildall). Предполагалось собственными силами сопровождать компилятор и вносить в него изменения по мере развития вычислительных средств. Так и вышло: компилятор прошел весь путь развития от MS-DOS 1.0 до Win32, притом, что сам автор более не занимался этой темой, а в 1994 году погиб. Так что этот компилятор можно считать отечественной разработкой, особенно с учетом большого числа изменений, используемых только здесь. Сопровождение компилятора, помимо массы хлопот имеет и ряд преимуществ, благодаря которым он продолжает и поныне развиваться и использоваться.
Но статья не о развитии собственно компилятора, а об изменениях языка программирования как своеобразном «побочном эффекте» работы с компилятором. Вероятно, у каждого программиста возникали мысли о том, что хорошо бы внести то или иное улучшение в используемые средства, в том числе и в язык. Эта тема постоянно обсуждается на компьютерных форумах, причем есть дельные и интересные предложения. Но самостоятельные доработки трансляторов большинству недоступны, а разработчики систем программирования, связанные к тому же коммерческими интересами, вопросами стандартизации и совместимости, похоже, обращают мало внимания на такие предложения.
В данном случае эти проблемы отсутствовали, причем основным занятием было не сопровождение компилятора, а решение широкого круга задач в Ракетно-космической корпорации «Энергия». Далее приводится сводка результатов многолетнего эксперимента на тему того, как бы программисты переделали стандарт. Большую роль здесь играет субъективный фактор и, вероятно, люди с другими взглядами внесли бы другие изменения. Все это так, но, во-первых, все приводимые изменения – свершившийся факт, а не просто высказанные пожелания, они реально используются, некоторые уже много лет, во-вторых, изменения вводились в результате выявления проблем при решении конкретных задач. Успешное решение этих задач с помощью новых возможностей вселяло уверенность, что «самодеятельное» развитие языка идет по правильному пути.
Язык PL/1 не знаком большинству сегодняшних программистов. Несмотря на это, в статье не объясняются все его возможности, а описываются лишь изменения относительно стандарта [3] или стандарта упрощенного варианта [4]. Но поскольку PL/1 классический императивный универсальный язык программирования, можно надеяться, что читателям будет несложно разобраться, о каких возможностях и их расширениях идет речь.
Последние десятилетия, несмотря на быстрое развитие вычислительных средств, на практике пришлось иметь дело почти только с процессорами архитектуры IA-32, являющейся потомком архитектуры IA-16. Первый процессор этой архитектуры, Intel 8086, был выпущен в 1978 году. При его разработке, безусловно, обращалось внимание и на поддержку языков высокого уровня, а в середине 70-х в США PL/1 активно внедрялся фирмой IBM и относился как раз к языкам, которые надо поддерживать архитектурно. Поэтому многие его понятия нашли прямое отражение в командах IA-16/32.
PL/1 оперирует числами с плавающей и фиксированной точками, что соответствует командам плавающей арифметики, целочисленной арифметики (для целых) и двоично-десятичной арифметики (точные числа с дробной частью). PL/1 оперирует строками, что хорошо соответствует «цепочечным» командам процессора. PL/1 использует логические операции с битовыми строками, что соответствует всем логическим операциям процессора. Наконец, такие объекты языка как процедуры-переменные и метки-переменные строго отображаются в процессоре в команды косвенных вызовов и косвенных переходов. Даже оператор TRANSLATE очень похож на команду «перекодировки» XLAT.
Но есть команда процессора, не имеющая отображения в PL/1. Это команда обмена XCHG как одновременного двунаправленного присваивания. Оператор взаимного присваивания затребован, например, в алгоритмах сортировки или при работе с такими объектами как семафоры. Поэтому в язык был добавлен оператор обмена в виде: XY;
Из-за ограничения в символах знак операции обмена был сделан из трех символов: «<», «=» и «>», что оказалось самым наглядным вариантом.
Если объекты занимают 1, 2 или 4 байта, обмен производится одной командой XCHG и двумя MOV (примеры приводятся вместе с генерацией кода):
DECLARE (X,Y) FIXED(31); X<=>Y; A108000000 mov eax,X F087050C000000 xchg eax,Y A308000000 mov X,eax |
Для более длинных объектов применяется цикл из команд обмена. Типы и размерности объектов должны строго совпадать, так как из-за преобразований простая команда обмена станет невозможной. Для использования в семафорах генерируется префикс блокировки шины LOCK (байт F0). На практике оператор обмена применялся и для объектов размером в байт, и к фрагментам файлов «отображаемых на память» размером в сто мегабайт.
Данный оператор улучшил отображение понятий языка на команды процессора, увеличил эффективность программы и наглядность текста при небольших изменениях в компиляторе.
Стандарт языка [3] позволяет писать операторы вида A=CONSTANT; где A – массив, т.е. работать со сложными объектами как с единым целым. В стандарт [4] эти возможности уже не включены. Однако наличие «цепочечных» команд STOS и SCAS позволяет в части обнуления частично вернуться к таким операторам, причем реализация обнуления проста.
Поэтому к обычным операторам пересылки массивов типа:
DECLARE (X,Y) (1:10) FLOAT(53); X=Y; BE58000000 mov esi,offset Y BF08000000 mov edi,offset X B914000000 mov ecx,20 F2 A5 rep movsd |
Или к обычным операторам сравнения массивов типа:
IF X^=Y THEN ... BE58000000 mov esi,offset Y BF08000000 mov edi,offset X B950000000 mov ecx,80 F3A6 repe cmpsb 0F95C0 setne al 3C00 cmp al,0 7405 je @1 |
Добавлены операторы обнуления как единого целого:
X=0; BF08000000 mov edi,offset X B914000000 mov ecx,20 33C0 xor eax,eax F2AB rep stosd |
и операторы сравнения с нулем как единого целого:
IF X^=0 THEN ... или IF X=0 THEN ... BF08000000 mov edi,offset X B950000000 mov ecx,80 32C0 xor al,al F3AE repe scasb 0F95C0 setne al или 0F94C0 sete al 3C00 cmp al,0 7405 je @2 |
Еще добавлены и специальные операторы обнуления. На практике часто требовалось, чтобы первоначально отдельная программа (EXE-файл) со временем включалась бы как процедура в общий модуль большого проекта. Однако если при первом вызове такой процедуры ее локальная явно не инициализированная память содержит, как и раньше, нули, то при последующих вызовах могли происходить ошибки, поскольку эта память уже не содержит исходных нулей. Для облегчения перевода программ в процедуры был добавлен оператор обнуления локальных данных подпрограммы одним действием:
TEST:PROCEDURE(A,B,C);
DECLARE...
TEST=0; // КАЖДЫЙ РАЗ ОБНУЛЯЕМ ВСЕ ЛОКАЛЬНЫЕ ДАННЫЕ
... |
Доработка компилятора незначительна, поскольку он имеет и размер, и адрес начала блока локальных данных каждой процедуры, и использует их, например, для запоминания при рекурсии. Новый оператор обнуления внешне похож на присвоение результату функции в Паскале.
И еще одной новой формой оператора обнуления является автоматический сброс значения указателя в операторе освобождения памяти:
DECLARE P PTR; DECLARE X(10) FLOAT(53) BASED; ... FREE P->X; 33DB xor ebx,ebx F0871D08000000 xchg ebx,P E800000000 call ?FREOP |
Использование команды XCHG вместо MOV позволяет автоматически убирать «висячие ссылки» после освобождения ранее выделенной памяти, хотя, конечно, полностью снять проблему неверных ссылок и не может.
В обнулении есть тонкость. В целом, заполнение нулевыми байтами соответствует логическому понятию «нуля» в языке. Это годится и для чисел с плавающей и фиксированной точкой, и для целых чисел, и для строк переменной длины (строка нулевой длины считается пустой), и для битовых строк, и даже для указателей понятие нуля и NULL идентичны. И только строка постоянной длины считается в стандарте пустой, если она заполнена пробелами, а не нулями. Но поскольку нулевой символ отображается как пробел и на дисплее, и на распечатке, а встроенная функция TRIM была доработана, чтобы воспринимать нули как пробелы, с таким несоответствием можно мириться, даже учитывая потенциальную возможность ошибок.
На практике часто требуется выполнить группу операторов только один раз, причем управление проходит через это место в программе более одного раза. Разумеется, можно использовать условный оператор, однократно меняющий свое условие внутри себя. Но в данном случае для подобных целей введен специальный оператор в виде префикса «1» перед составным оператором или оператором цикла, который в PL/1 ограничен словами DO ... END; Поэтому любую группу и любой цикл можно сделать одноразовыми с помощью конструкции:
1 DO ... END; |
При генерации кодов такая конструкция превращается в системный вызов, который при получении управления превращает себя в команду перехода, и затем управление будет обходить всю группу операторов.
Форма записи типа X+=2; вместо X=X+2; принята после появления языка Си. Даже фирма IBM дополнила свой транслятор PL/1 в этой части [2]. И в данном компиляторе тоже все операции, имеющие два операнда и результат, помещаемый в первый, могут записываться в сокращенном виде. Это относится и к операции склейки строк:
DECLARE S CHAR(*) VAR;
S||=’ ’; // ДОПИСЫВАЕМ ПРОБЕЛ В КОНЕЦ СТРОКИ S |
В старой реализации PL/1 для компьютеров «Wang» даже вводили новое ключевое слово MODULE, чтобы улучшить модульность программ, так как в стандарте независимой единицей трансляции является процедура, а не модуль. Оказалось, что достаточно просто разрешить трансляцию отдельного BEGIN-блока, поскольку он полностью соответствует понятию модуля. Не потребовалось ни новых ключевых слов, ни, тем более, новых понятий в языке.
По мысли разработчиков PL/1, все операторы языка (за исключением присваивания) должны были обязательно начинаться с ключевого слова. На практике оказалось, что ключевое слово оператора вызова CALL можно сделать необязательным, и ни к каким плохим последствиям это не приводит. Конечно, тогда имена процедур уже не могут совпадать с некоторыми ключевыми словами. Но, во-первых, так никто на практике процедуры и не называет, а во-вторых, в таких случаях всегда можно вернуться к использованию CALL.
Работа с Win32 API размыла границу между процедурой и функцией, поскольку большинство процедур в Win32 – формально функции, возвращающие ответ. Фактически же это процедуры, меняющие при выполнении среду (т.е. выполняемые ради побочного эффекта), а их результат – лишь рапорт о выполнении. В компилятор была введена возможность обращаться к функциям как к процедурам, т.е. игнорировать результат.
Например, вызов объекта как функции:
//---- НАЧИНАЕМ РИСОВАТЬ СПРАЙТ ----
IF BEGINSPRITE(SPRITE,0)^=0 THEN
PUT SKIP LIST(’ОШИБКА НАЧАЛА РИСОВАНИЯ СПРАЙТА’); |
Использование вызова того же объекта как процедуры:
//---- НАЧИНАЕМ РИСОВАТЬ СПРАЙТ ----
BEGINSPRITE(SPRITE,0); |
Операторы цикла в PL/1 довольно развитые, и доработки связаны не с их неудобством, а с доведением до логического конца заложенных в язык идей. Единственный недостаток – отсутствие в стандарте оператора следующей итерации CONTINUE, который и был добавлен в компилятор одной из первых доработок в пару к имеющемуся в стандарте оператору выхода из цикла LEAVE.
Следующей несложной доработкой циклов стало введение более простой конструкции «бесконечного» цикла вместо заголовка типа WHILE(’1’B). Для этой цели используется единственное и уже имеющееся ключевое слово REPEAT. Бесконечный цикл выглядит как:
DO REPEAT; ... END; |
Еще одной простейшей доработкой языка в части циклов стало расширение применения необязательного имени после END. Т.е. если в языке можно писать имя в конце подпрограммы:
TEST:PROCEDURE; ... END TEST; |
то точно также можно писать и имена параметров в конце циклов, в духе предложений [1], например:
DO I=1 TO 100; DO J=1 TO 100; ... END J; END I; |
или вставляя в конец ключевые слова REPEAT/WHILE:
DO REPEAT; ... END REPEAT; |
что повышает наглядность и «ошибкоустойчивость» текста программы. Причем стандартная возможность с меткой также сохраняется:
M1: DO REPEAT; ... END M1; |
Более серьезной доработкой стало расширение возможностей «сложных» (составных) циклов, имеющих перечисление через запятую отдельных «простых» циклов в своем заголовке. Идея «сложных» циклов была доведена до уровня полной независимости каждого «простого» цикла в заголовке. Теперь можно менять не только закон приращения параметра очередного цикла, но и сам этот параметр цикла, например, вот задание «квадратной» траектории:
Y=0; DO X=0 TO 99, Y=0 TO 99, X=100 TO 1 BY -1, Y=100 TO 0 BY -1; ПЕРЕМЕСТИТЬ(X,Y); END; |
Здесь 4 цикла с двумя параметрами, и когда один из них меняется, второй – «замораживается» и остается таким, каким был при выходе из предыдущего «простого» цикла.
Доработанные циклы оказались даже нагляднее конструкции UNTIL, когда первую итерацию нужно выполнять всегда:
//---- цикл итераций для подбора Axe ---- do Axe=a1/(1e0+j*c2u*si2), while(abs(last-Axe)>=eps); last = Axe; // предыдущее значение полуоси j = Em/Axe/Axe; // новая часть возмущения Axe = a1/(1e0+j*c2u*si2); // очередное значение полуоси end while; |
Еще один пример из реальной задачи – нахождение полного приращения DF функции F от шести переменных при решении в частных производных. Вместо длинной записи с шестью вызовами функции F можно использовать «сложный» цикл, причем даже не цикл в обычном понимании, а просто выделение повторяющейся части алгоритма с разными параметрами:
DF=0; X=X0; Y=Y0; Z=Z0; U=U0; V=V0; W=W0; F0=F(X,Y,Z,U,V,W); DO X=X0+DX, Y=Y0+DY, Z=Z0+DZ, U=U0+DU, V=V0+DV, W=W0+DW; DF+=F(X,Y,Z,U,V,W)-F0; X=X0; Y=Y0; Z=Z0; U=U0; V=V0; W=W0; END; |
На практике оказалось, что оператор окончания программы STOP удобнее, если после него может быть необязательное целое выражение в скобках, например STOP(10); Тогда это значение будет передано операционной системе как код окончания всей программы и может быть использовано, например, «скриптом», запустившим программу.
Добавлена также возможность писать STOP прямо в конце операторов текстовой выдачи с целью сокращения числа операторных «скобок», т.е. вместо:
IF I=0 THEN DO; PUT SKIP LIST(’ОШИБКА’); STOP(-1); END; |
можно писать:
IF I=0 THEN PUT SKIP LIST(’ОШИБКА’, STOP(-1)); |
В языке имеется удобная встроенная функция поиска образца в строке INDEX, возвращающая номер позиции найденного вхождения образца или ноль. Но часто нужно найти не первое, а следующие вхождения, и для этого удобнее иметь необязательный третий параметр, показывающий с какого места в строке нужно начать поиск, например, найти последнюю запятую в строке S:
DO I=0, WHILE(I>0); J=I; I=INDEX(S,’,’,I+1); END WHILE; IF J>0 THEN PUT LIST(’ПОСЛЕДНЯЯ ЗАПЯТАЯ В ПОЗИЦИИ’,J); |
В релизе транслятора фирмы IBM [5] как новость упоминается такая же возможность, притом, что в описываемом компиляторе INDEX с тремя параметрами используется еще с ноября 1993 года.
Для отладочных выводов оказалось удобным иметь константы, автоматически заполняемые компилятором. Имена таких констант решено начинать со знака вопроса, чтобы не путаться с идентификаторами программы. Используются следующие константы:
С такими константами удобно создавать универсальные отладочные операторы печати, которые могут копироваться в разные части исходного теста.
Поскольку в исходном компиляторе комплексных чисел не было, при их реализации было решено максимально упростить эту часть языка. На практике комплексные числа требовались в научных расчетах, поэтому было решено использовать их только в формате с плавающей точкой. Комплексные константы в стандарте [3] рассматривались как выражения, т.е. даже в простом операторе X=1+2i; единица дополнялась нулевой мнимой частью, затем двойка дополнялась нулевой действительной частью и только потом эти два комплексных числа складывались. Для исключения таких преобразований было решено писать комплексные константы в кавычках, как строку: X=’1+2i’; что позволило компилятору разбирать их уже как единые объекты. Но если у константы нет действительной части, ее можно писать и без кавычек. Удалось также обойтись и без встроенных функций COMPLEX, REAL, IMAG, CONJG, вместо которых используется просто «наложение» объектов в памяти:
DECLARE X FLOAT(53) COMPLEX, 1 Y DEFINED(X), 2 R FLOAT(53), // ДЕЙСТВИТЕЛЬНАЯ ЧАСТЬ X 2 IM FLOAT(53); // МНИМАЯ ЧАСТЬ X //---- ЗАМЕНА ВСТРОЕННОЙ ФУНКЦИИ COMPLEX ---- R=1; IM=2; // ПОЛУЧЕНИЕ КОМПЛЕКСНОГО X=1+2i //---- ЗАМЕНА ВСТРОЕННОЙ ФУНКЦИИ REAL ---- X=’1+2i’; // СРАЗУ ПОЛУЧАЕМ ДЕЙСТВИТЕЛЬНУЮ ЧАСТЬ В R=1 //---- ЗАМЕНА ВСТРОЕННОЙ ФУНКЦИИ IMAG ---- X=’1+2i’; // СРАЗУ ПОЛУЧАЕМ МНИМУЮ ЧАСТЬ В IM=2 //---- ЗАМЕНА ВСТРОЕННОЙ ФУНКЦИИ CONJG ---- IM=-IM; // ПОЛУЧЕНИЕ СОПРЯЖЕННОГО ЧИСЛА X=1-2i |
Оказалось удобным иметь в языке оператор LEAVE ON, который действителен только внутри обработчика исключений и аналогичен оператору RETURN внутри процедуры или функции. Использование оператора LEAVE ON позволило, не вводя новых ключевых слов отказаться от операторов перехода на конец обработчика.
На практике оказалось неудобным, что в стандарте языка [3] имеется параллельный запуск процедуры как задачи, но нет принудительного завершения. Поэтому оператор параллельного запуска типа:
CALL TEST TASK; или просто TEST TASK; |
был дополнен оператором снятия задачи:
CALL TEST STOP; или просто TEST STOP; |
позволяющим вызывающей процедуре в любой момент остановить вызванную. Был упрощен и оператор синхронизации WAIT, аргументом которого теперь является не специальный объект типа «событие», а простая логическая переменная (битовая строка в один бит).
В языке PL/1 есть сложности с выходными параметрами процедур. Правила преобразований, в том числе по умолчанию, могут привести к тому, что из-за преобразований, которых программист не ожидал или о которых забыл, вычисления внутри тела процедуры попадут не в выходной параметр, как хотел программист, а в его копию, созданную компилятором. А сам выходной параметр не изменит своего значения. Поэтому добавлен символ «*» как необязательный признак контроля выходного параметра в операторе вызова процедуры или функции. Если в программе применить вызов, например, TEST(A,*B);, и в процессе трансляции из-за преобразований будет создана копия переменной B (т.е. данный параметр не может быть выходным), компилятор выдаст соответствующее предупреждение.
Иногда для управления аппаратурой требуются обращения к портам ввода/ввода и регистрам. Хотя есть возможность использования модулей на ассемблере, были внесены два дополнения, позволяющие напрямую работать с командами и регистрами.
Во-первых, были расширены возможности встроенной функции UNSPEC, которая теперь может встречаться вне оператора присваивания, и тогда генерирует произвольные коды в программе. Например, код исключения отладки в IA-32 (команда INT 3) отображается как UNSPEC(’CC’B4), а код команды чтения из порта (команда IN AL,DX) отображается как UNSPEC(’EC’B4).
Во-вторых, появились служебные переменные, начинающиеся со знака «?» и обозначающие регистры IA-32.
Становятся допустимыми конструкции типа:
//---- ВЫХОД В ОТЛАДЧИК, ЕСЛИ ПЕРЕМЕННАЯ X ОТРИЦАТЕЛЬНА -- IF X<0 THEN UNSPEC(’CC’B4); //---- ЧТЕНИЕ ИЗ ПАРАЛЛЕЛЬНОГО ПОРТА В ПЕРЕМЕННУЮ X ---- DECLARE X BIT(8),?DX BIT(16),?AL BIT(8); ?DX=’0378’B4; UNSPEC(’EC’B4); // ЧТЕНИЕ IN AL,DX X=?AL; 66BA7803 mov dx,888 EC cop 236 A208000000 mov X,al |
Для использования Win32 API, а также для реализации процедур-«обратных» вызовов Windows потребовалось ввести ключевое слово IMPORT в заголовке процедуры, которое указывает компилятору, что передача параметров и прием результата идет по правилам Win32. Проблемой оказалась и несовместимость имен: в PL/1 у идентификаторов строчные и прописные буквы не различаются, к тому же в DLL-библиотеках встречаются имена длиннее 31 символа. Было решено иметь в системной библиотеке специальные таблицы «псевдонимов», устанавливающие соответствие между именами, которые используются в PL/1 программе и именами, которые реально имеются в DLL-библиотеках. Такие таблицы уже заранее составлены для наиболее используемых библиотек вроде kernel32.dll или user32.dll.
С точки зрения языка процедуры из динамически подключаемых библиотек имеют атрибуты ENTRY VARIABLE EXTERNAL и именно эти атрибуты автоматически добавляются в случае указания атрибута IMPORT, например:
DECLARE
MOVEWINDOW ENTRY(FIXED(31), FIXED(31), FIXED(31), FIXED(31),
FIXED(31), FIXED(31))
RETURNS(FIXED(31)) IMPORT;
//---- ЗАДАЕМ РАЗМЕРЫ НАШЕГО ОКНА ПО ЭКРАНУ ----
MOVEWINDOW(HWND,0,0,РАЗМЕР(1)-1,РАЗМЕР(2)-1,-1); |
В случае загрузки процедур во время исполнения, таблицы «псевдонимов» не требуются:
//---- ДИНАМИЧЕСКИ ЗАГРУЖАЕМ ФУНКЦИЮ ИНИЦИАЛИЗАЦИИ ---- DCL DIRECT3DCREATE9 ENTRY(FIXED(31)) RETURNS(PTR) IMPORT; ?ЗАГРУЗИТЬ_ИЗ_DLL ('D3D9.DLL','Direct3DCreate9',ADDR(DIRECT3DCREATE9)); //---- САМО ОБРАЩЕНИЕ К ПРОЦЕДУРЕ СОЗДАНИЯ ИНТЕРФЕЙСА ---- P_ИНТЕРФЕЙС=DIRECT3DCREATE9(D3D_SDK_VERSION); |
В целом, введенные доработки позволяют использовать процедуры Win32 в полной мере, без ограничений из-за использования PL/1.
Кроме различных изменений и дополнений во встроенных функциях языка потребовалось и введение новых функций, обычно добавляемых в системную библиотеку без исправлений компилятора (кроме внесения имени в таблицу).
Новая встроенная функция DATE4Y возвращает четыре цифры года и решает «проблему тысячелетия», а функция LDATE достает дату без «лишнего» обращения к Windows, исключая потенциальную ошибку полуночи.
Новые встроенные функции ASIND и ACOSD (возвращающие ответ в градусах) убирают непоследовательность стандарта, поскольку имеются ATAN и ATAND, COS и COSD, а вот ACOSD/ASIND как пары к ACOS/ASIN – почему-то нет.
На практике потребовалась и введение новой функции REPLACE, работающей аналогично TRANSLATE, но меняющей не отдельные символы, а заданные образцы строк, например оператор:
S=REPLACE(S,’палки’,’елки’); |
заменит в строке S все вхождения образца «елки» на образец «палки», причем результирующая строка в общем случае будет иметь длину, отличную от исходной.
Одна из новых функций обязана своим появлением удобной команде процессора FSINCOS, выдающей значения синуса и косинуса одновременно. При реализации этой команды как встроенной функции SINCOS (и SINDCOSD) для сохранения системы тригонометрических функций, имеющих всегда один операнд и один результат, было принято, что функция SINCOS возвращает значение как комплексное число. Это оказалось удачным решением, поскольку пара синуса и косинуса часто требуется на практике именно как комплексное число.
Потребовалась и новая встроенная функция обработки исключений ONTERM, возвращающая число правильно считанных объектов из списка GET DATA и GET LIST, поскольку если в этих операторах указаны большие списки переменных, бывает затруднительно быстро найти тот элемент в списке, который при чтении вызвал исключение.
Были расширены применения некоторых уже имеющихся функций. Так, оказалось удобным разрешить обращение к встроенной функции LENGTH не только для строк, но и для файлов, особенно «отображаемых на память». А функция ROUND стала применима и к битовым строкам, для которых она генерирует команду циклического сдвига:
DECLARE B BIT(8); B=ROUND(B,4); // ПЕРЕСТАНОВКА «ТЕТРАД» В БАЙТЕ МЕСТАМИ |
В стандарте размер данных при обмене с помощью операторов READ и WRITE определяется размером указанных в этих операторах переменных. Но это оказалось неудобным, если размер передаваемых данных заранее неизвестен и определяется ими самими. Например, в читаемом файле указана длина элемента, а затем идет сам элемент указанной длины. В таком случае приходилось организовывать цикл чтения, например в массив объектов размером в байт. Поэтому в операторы READ и WRITE был добавлен необязательный второй аргумент – явная длина в байтах:
//---- ЧИТАЕТСЯ ЧИСЛО БАЙТ ПО РАЗМЕРУ ПЕРЕМЕННОЙ X ---- READ FILE(F) INTO(X); //---- ЧИТАЕТСЯ X БАЙТ, НЕЗАВИСИМО ОТ РАЗМЕРА A ---- READ FILE(F) INTO(A,X); |
Конечно, в таких случаях программист должен сам следить за тем, чтобы размер переменной не был меньше числа передаваемых байт.
Язык PL/1 имеет множество ключевых не зарезервированных слов, и как показала практика, не все они удачны или необходимы. Например, выглядит излишним ключевое слово KEYFROM в операторе WRITE вместо просто KEY (как в операторе READ),.поэтому слова KEY и KEYFROM сделаны эквивалентными. Ключевое слово OPTIONS в заголовке процедур и ENVIRONMENT в операторе OPEN обозначают одно и то же – параметры внешней среды, поэтому они тоже сделаны эквивалентными. Также разрешено использовать в операторах DECLARE ключевые слова PROC/PROCEDURE вместо ENTRY, поскольку в стандарте сокращенной версии [4] нет дополнительных точек входов. И в операторе перехода GO TO можно не писать TO.
А для файлов применен такой же подход, что и для описания размерности массива: если явно не указано ключевое слово, размерность должна быть обязательно в начале описания:
DECLARE X(1:10) FLOAT(53); DECLARE X FLOAT(53) DIMENSION(1:10); |
В данном компиляторе точно так же ключевое слово FILE можно не указывать в операторах обмена, но тогда его имя должно идти в начале оператора:
OPEN (F) PRINT; OPEN PRINT FILE(F); |
Необязательным стало и ключевое слово ERROR в обработчиках исключительных ситуаций, т.е. вместо ON ERROR(35)... можно писать просто ON(35) ...
Такие допущения упрощают написание программы на PL/1.
В стандарте разрешено объединять однотипные операторы в один с помощью запятых, т.е. например, вместо:
CLOSE FILE(F1); CLOSE FILE(F2); |
можно писать короче:
CLOSE FILE(F1),FILE(F2); или CLOSE(F1),(F2); |
Однако в стандарте так не сделано для операторов исключительных ситуаций, имеющих одинаковые «тела». В данном компиляторе такие операторы тоже можно объединять:
ON ENDFILE(F), CONVERSION, ERROR(35) STOP; |
Стандарт требовал, чтобы в операторе ALLOCATE явно указывались имена объектов, которым выделяется память. Однако на практике нужно выделять и память для передачи как параметра процедуры без указания имени. Поэтому была внесена доработка: если в операторе указывается не имя, а выражение в скобках, то выделяется заданное число байт и адрес начала записывается в указатель:
ALLOCATE(100000) SET(P); |
В транслятор IBM VisualAge PL/I были внесены подобные изменения, но с изменением и первоначального синтаксиса оператора:
P=ALLOCATE(100000); |
В данном компиляторе в отличие от стандарта коэффициент повторения перенесен в конец строки, например, четыре байта единичных бит можно записать как ’1’(32)B. Это упростило компилятор, но главное, коэффициент повторения строчной константы перестал путаться с коэффициентом повторения при инициализации начальных значений. Например:
DECLARE Х(10) CHAR(5) STATIC INITIAL((10) ’*’(5)); |
здесь внутри атрибута INITIAL (10) – это повторение констант в массиве, а (5) – повторение символа в константе.
Было добавлено автоматическое определение длины строк по максимальной длине инициируемых значений, например:
DECLARE S(1:4) CHAR(*) VAR STATIC INIT(’СЕВЕР’,’ЮГ’,’ЗАПАД’,’ВОСТОК’); |
Компилятор назначит длину строк в массиве S равной 6.
В данном компиляторе можно писать числовые константы, используя для удобства чтения знак подчеркивания (как и в языке Ада), например: X=1_000_000;
Еще одной особенностью стало разрешение использовать «псевдографические» символы там, где разрешены пробелы, причем они и воспринимаются компилятором как пробелы (если не стоят внутри текстовых констант). Это позволяет применять всякие «уголки» и «полочки» в исходном тексте, например, для выделения циклов на нескольких строках. Такое оформление облегчает анализ текста. Нашлось применение и не определенным в стандарте фигурным скобкам. Было принято, что открывающая фигурная скобка эквивалентна слову DO, а закрывающая – END, т.е. можно писать в стиле, напоминающим язык Си, например:
IF X=0 THEN {; Y=1; Z=2; }; |
Комментарии, определенные стандартом, оказались не совсем удобными: во-первых, отсутствовали однострочные комментарии, во-вторых, нельзя было закомментировать фрагмент, имеющий собственные комментарии, т.е. комментарии нельзя было вкладывать друг в друга.
Поэтому введены однострочные комментарии, начинающиеся с //. Теперь уже нельзя ставить обычные комментарии вплотную к знаку деления как раньше (проблема, аналогичная делению на указатель в языке Си: получается символ комментария). Несмотря на эту потенциальную опасность ошибки, необходимость иметь в языке однострочные комментарии очевидна.
Проблема же вложенных комментариев была частично решена с помощью также не определенных в стандарте квадратных скобок. Встретив в комментарии символ открывающей квадратной скобки, компилятор пропускает все символы до закрывающей квадратной скобки, в том числе и пары символов /* ... */. Такой прием позволяет комментировать фрагменты с комментариями внутри.
Были также введены препроцессорные операторы для управления подробностью отладочной печати, которые задаются в тексте программы как числа %0 ... %9 обычно в начале строки. При запуске трансляции можно задать число – «уровень» отладочной печати. Если число %0 ... %9 меньше или равно данному уровню – компилятор превращает его в два пробела, иначе в две косых черты (в однострочный комментарий). Это позволяет иметь вывод с разной степенью подробности и варьировать его уровнем при трансляции. Эту же возможность можно применять и просто для получения разных вариантов программы без редактирования ее текста.
Из-за того, что PL/1 был рассчитан на англоязычную среду, потребовалось добавить кириллицу. Причем сразу ставилась задача свободного ее использования, в том числе и в ключевых словах (см. раздел ниже). Появление «кириллицы Windows» усложнило кодировку, так как в консольном окне осталась «кириллица DOS». Это привело к использованию текстовых строк-констант вместе с «квалификатором» W, обозначающего кодировку Windows, например:
DECLARE S1 CHAR(*) VAR STATIC INITIAL (’Привет!’), S2 CHAR(*) VAR STATIC INITIAL (’Привет!’W); |
Первый символ строки S1 имеет код 8F.
Первый символ строки S2 имеет код СF.
Для операторов выдачи PUT DATA, содержащих идентификаторы на кириллице, были добавлены препроцессорные операторы %D и %W, переводящие PUT DATA в соответствующую кодировку и действующие до появления следующего подобного препроцессорного оператора.
PL/1 имеет множество ключевых слов, которые должны были сделать текст похожим на инструкцию на английском. Поэтому при русификации нужны и русские аналоги английских слов, чтобы в пределе было возможно написание исходного текста на PL/1 без использования латиницы. На объектный модуль использование русских и/или латинских слов не оказывает влияния, а с помощью формальных преобразований всегда можно вернуться к стандартной и совместимой форме исходного текста не просматривая его.
Русификация реализована как предопределенная таблица значений для ключевых слов и встроенных функций, каждая из строк которой эквивалентна препроцессорному оператору %REPLACE и содержит элементы типа:
ЕСЛИ BY IF, ТОГДА BY THEN, ИНАЧЕ BY ELSE, |
На рисунке представлен фрагмент исходного текста реальной программы с учетом изложенных выше особенностей.
Рисунок 1. Фрагмент исходного текста PL/1 программы с русскими эквивалентами ключевых слов
Рассмотренные в статье расширения языка составляют лишь небольшую часть от проведенных работ по развитию системы программирования на PL/1, поскольку основные затраты пришлись на переработку компилятора, разработку библиотек, средств отладки и т.п. Однако без изменений в самом языке многие решения так и остались бы неэффективными, неудобными или просто неудачными. Легко заметить, что все изменения носят эволюционный характер и о создании нового языка PL/2 речь не идет.
Совершенствование языка положительно сказалось на разработке, отладке и эксплуатации программ, многие находки выдержали проверку временем. Есть и положительный психологический эффект просто от сознания того, что можно развивать используемый язык и исправлять неустраивающие детали, а не иметь дело с застывшим раз и навсегда стандартом, консервирующим все неудобства. Конечно, трудно количественно оценить выигрыш, полученный в результате внесенных изменений, можно лишь сослаться на конечный успех больших и малых проектов, созданных данными средствами, включая программы, работающие сейчас на борту Международной космической станции.
И в свете постановления о создании Национальной Программной Платформы, если это понятие трактовать не просто как обязательное использование Lunix, а как собственные идеи и их реализацию, эта система программирования, пожалуй, вполне подходит в качестве одного из элементов национальной платформы, поскольку ее развитие происходит в нашей стране, независимо от развития подобных систем за рубежом.
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |