Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Прочее

К вопросу о совершенствовании языка программирования

Введение

Введение

            В статье приводятся работы, выполненные небольшим коллективом программистов на протяжении ряда лет. Сложилось так, что еще в 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 как одновременного двунаправленного присваивания. Оператор взаимного присваивания затребован, например, в алгоритмах сортировки или при работе с такими объектами как семафоры. Поэтому в язык был добавлен оператор обмена в виде: X↔Y;

            Из-за ограничения в символах знак операции обмена был сделан из трех символов: «<», «=» и «>», что оказалось самым наглядным вариантом.

            Если объекты занимают 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

            В языке имеется удобная встроенная функция поиска образца в строке 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 года.

Встроенные константы времени компиляции

            Для отладочных выводов оказалось удобным иметь константы, автоматически заполняемые компилятором. Имена таких констант решено начинать со знака вопроса, чтобы не путаться с идентификаторами программы. Используются следующие константы:
  • ?DATE- дата сборки программы как строка из 8 символов.
  • ?MD - имя модуля программы
  • ?FILE - имя файла с исходным текстом программы
  • ?PROC - имя текущей процедуры
  • ?LINE - номер текущей строки исходного текста
  • ?INCL - имя текущего файла оператора %INCLUDE
  • ?VER - номер текущей версии компилятора
            С такими константами удобно создавать универсальные отладочные операторы печати, которые могут копироваться в разные части исходного теста.

Упрощение комплексных чисел

            Поскольку в исходном компиляторе комплексных чисел не было, при их реализации было решено максимально упростить эту часть языка. На практике комплексные числа требовались в научных расчетах, поэтому было решено использовать их только в формате с плавающей точкой. Комплексные константы в стандарте [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 как с процедурами PL/1

            Для использования 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, выдающей значения синуса и косинуса одновременно. При реализации этой команды как встроенной функции SINCOSSINDCOSD) для сохранения системы тригонометрических функций, имеющих всегда один операнд и один результат, было принято, что функция SINCOS возвращает значение как комплексное число. Это оказалось удачным решением, поскольку пара синуса и косинуса часто требуется на практике именно как комплексное число.

            Потребовалась и новая встроенная функция обработки исключений ONTERM, возвращающая число правильно считанных объектов из списка GET DATA и GET LIST, поскольку если в этих операторах указаны большие списки переменных, бывает затруднительно быстро найти тот элемент в списке, который при чтении вызвал исключение.

            Были расширены применения некоторых уже имеющихся функций. Так, оказалось удобным разрешить обращение к встроенной функции LENGTH не только для строк, но и для файлов, особенно «отображаемых на память». А функция ROUND стала применима и к битовым строкам, для которых она генерирует команду циклического сдвига:
DECLARE B BIT(8);
B=ROUND(B,4); // ПЕРЕСТАНОВКА «ТЕТРАД» В БАЙТЕ МЕСТАМИ

Доработка операторов обмена READ/WRITE

            В стандарте размер данных при обмене с помощью операторов 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,
На рисунке представлен фрагмент исходного текста реальной программы с учетом изложенных выше особенностей.
Исходный текст программы на русифицированном PL/1

Заключение

            Рассмотренные в статье расширения языка составляют лишь небольшую часть от проведенных работ по развитию системы программирования на PL/1, поскольку основные затраты пришлись на переработку компилятора, разработку библиотек, средств отладки и т.п. Однако без изменений в самом языке многие решения так и остались бы неэффективными, неудобными или просто неудачными. Легко заметить, что все изменения носят эволюционный характер и о создании нового языка PL/2 речь не идет.

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

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

Литература

1. «Ошибки программирования и приемы работы на языке ПЛ/1» Л.Ф. Штернберг, Москва «Машиностроение», 1993.
2. G.U.I.D.E. & SHARE Europe Joint Conference (10-13 October 1994, Vienna, Austria) «Power vs. Adventure – PL/I and C». Eberchard Sturm. Munster, Germany.
3. American National Standard: programming language PL/I. New York, NY, American National Standards Institute, 1979 (Rev 1998); 403p. ANSI Standard X3.53-1976.
4. American National Standard: information systems - programming language - PL/I general-purpose subset. New York, NY, American National Standards Institute, 1987; 449p. ANSI Standard X3.74-1987.
5. What’s new in Enterprise PL/I 4.1 and z/OS XL C/C++ V1R12 Peter Eldern, Chwan-Hang Lee IBM Corporation February 28, 2011.

Автор: Д.Ю.Караваев. 29.12.2011

Последняя правка: 2018-08-21    20:24

Оцените

Написать отзыв

Написать автору можно на электронную почту mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Каким должен быть язык программирования?

Анализ и критика

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Двадцать тысяч строк кода, которые потрясут мир?

Почему владение/заимствование в Rust такое сложное?

Масштабируемые архитектуры программ

Почему Хаскелл так мало используется в отрасли?

Бесплатный софт в мышеловке

Исповедь правового нигилиста

Русской операционной системой должна стать ReactOS

Почему обречён язык Форт

Программирование без программистов — это медицина без врачей

Электроника без электронщиков

Статьи Дмитрия Караваева

●  Идеальный транслятор

●  К вопросу о совершенствовании языка программирования

●  О реализации метода оптимизации при компиляции

●  О реализации метода распределения регистров при компиляции

●  О распределении памяти при выполнении теста Кнута

●  Опыты со стеком или «чемпионат по выполнению теста Кнута»

●  Сколько проходов должно быть у транслятора?

●  Чтение лексем

●  Экстракоды при синтезе программ

●  Об исключенных командах или за что «списали» инструкцию INTO?

●  Типы в инженерных задачах

●  Непрерывное компилирование

●  О русском языке в программировании

●  Формула расчета точности для умножения

●  Права доступа к переменным

●  Скорость в попугаях

●  Крах операции «Инкогнито»

●  Предопределенный результат

Компьютерный юмор

Прочее

Последние комментарии

2018/10/11 22:29, Автор сайта
Формула расчета точности для умножения

2018/10/08 14:00, Неслучайный читатель
Сколько проходов должно быть у транслятора?

2018/10/06 12:19, Автор сайта
Тексто-графическое представление программы

2018/10/04 17:39, Автор сайта
Об исключенных командах или за что «списали» инструкцию INTO?

2018/09/29 16:52, Автор сайта
Как отличить унарный минус от бинарного

2018/09/22 20:13, Д.Ю.Караваев
Идеальный транслятор

2018/09/22 12:32, Автор сайта
Типы в инженерных задачах

2018/09/22 12:20, Д.Ю.Караваев
О русском языке в программировании