Данный обзор посвящён чрезвычайно популярному в прошлом, но практически полностью забытому в настоящее время языку программирования PL/I. Между тем, многие свойства этого языка, на наш взгляд, заслуживают изучения и наше время, так как часть их периодически переизобретается различными авторами, причём часто в ухудшенном виде.
Язык PL/I является для автора самым любимым языком программирования, автор даже состоял в международной организации “Team PL/I” до фактического прекращения её деятельности. Однако, итоговый малоубедительный результат эволюции языка требует осмысления.
Некоторые необычные для современности свойства PL/I:
-
PL/I – очень большой и сложный язык, количество только ключевых слов в котором исчисляется сотнями, а его изучение программистами в большинстве случаев производилось не в полном виде, а ограничиваясь отдельными подмножествами языка. К счастью, ключевые слова при этом не являются зарезервированными, их использование зависит от контекста;
-
PL/I – язык очень высокого уровня (во всяком случае, для своего времени), который при этом был основным языком системного программирования в ряде операционных систем;
-
PL/I – язык с огромным количеством встроенных типов данных, при этом со статической, но полностью нестрогой типизацией;
-
PL/I имеет два различных вида блочной структуры программы;
-
основные компиляторы PL/I способны порождать исполняемый код для программ практически с любыми синтаксическими ошибками;
-
основные компиляторы PL/I способны взаимодействовать с СУБД DB2 и использовать SQL как подмножество PL/I.
История PL/I
Язык PL/I был разработан в IBM к 1964 году как часть семейства компьютеров System/360.
К тому времени главным достижением IBM в области языков программирования был Фортран, использовавшийся повсеместно для практического решения вычислительных задач; Министерство обороны США разработало Кобол, который применяли разные компании преимущественно для финансовых приложений (что, между прочим, заставляет задуматься о целеполагании самого Министерства обороны); а в мире академической науки, особенно европейской, был распространён разработанный международным комитетом язык Алгол, который учёные использовали для своих исследований и публикации алгоритмов, поскольку к практическому программированию он был по ряду причин не очень-то пригоден.
Разработчики IBM поставили перед собой задачу создать язык, который непосредственно включит в себя практически все свойства Фортрана, Кобола и Алгола, обеспечив механическое воспроизведение программ, написанных на любом из этих языков, а кроме того, будет содержать ещё всё возможное, что может оказаться в программировании практически полезным, причём компилятор должен в максимальной степени разумно пытаться интерпретировать всё, что захочет написать программист.
Результатом разработки стал язык не виданных ни до, ни после того размеров, практически полностью состоящий из того, что сейчас принято называть синтаксическим сахаром. Условно говоря, десятитонный грузовик синтаксического сахара. Настолько огромное его количество, что программиста могло этим сахаром засыпать с головой и ещё много бы осталось в грузовике.
IBM S/360 комплектовалась компилятором PL/I уровня F для OS/360 и компилятором подмножества PL/I для DOS/360. Компилятор PL/I (F) реализовывал очень объёмный язык PL/I, который был принят за внутрифирменный стандарт, при этом мог работать в 48 килобайтах оперативной памяти, реализуя трансляцию за несколько десятков проходов. Именно компилятор PL/I (F) получил огромную популярность в СССР, став доминирующим инструментом для программистов ЕС ЭВМ и фактически наиболее используемым средством профессионального программирования с 1970-х до начала 1980-х годов.
Тем временем, для семейства S/370 фирма IBM реализовала оптимизирующий компилятор PL/I (O) и отладочный компилятор PL/I, включающий средство интерактивной отладки программ в VM/CMS. Диалект языка PL/I (O) немного расширял возможности PL/I (F) и обеспечивал бо́льшую стройность конструкций языка. Фактически, именно диалектом PL/I (O) язык был представлен в период своего расцвета. В СССР компилятор PL/I (O) широко использовался в операционной системе СВМ, аналоге VM. Говоря далее о возможностях языка PL/I, мы в основном будем ориентироваться на PL/I (O) и отчасти на его естественные расширения 1990-х годов.
Ряд других компаний, кроме IBM, реализовали свои компиляторы PL/I, несколько отличающиеся по входному языку, и на PL/I была даже написана вне IBM весьма сложная и продвинутая по тем временам операционная система MULTICS. Но в целом эти проекты остались малоизвестны.
IBM реализовала также подмножество PL/I, язык PL/S, на котором написала операционную систему MVS. Затем на основе PL/S был создан PL/X, используемый для разработки z/OS. Компиляторы PL/S и PL/X не распространяются вне IBM.
В 1972 году Гари Килдалл из Intel разработал собственное крайне ограниченное подмножество PL/I – язык PL/M, и реализовал компилятор для CP/M. Впоследствии он основал собственную фирму Digital Research, и под её маркой выпустил написанный на ассемблере компилятор подмножества PL/I для CP/M-86 и MS-DOS 1.0. Впоследствии на его основе Д.Ю. Караваевым был реализован отечественный компилятор PL/1-КТ, нашедший своё применение в космической сфере.
Конгда стало понятно, что всё больше программ утекают с мейнфреймов на рабочие станции, в начале 1990-х годов IBM реализовала компилятор PL/I Workstation для AIX, OS/2 и Windows. Он практически полностью поддерживал диалект PL/I (O) и позволял в большинстве случаев простой перекомпиляцией запускать на рабочих станциях программы, написанные для мейнфреймов. Также в этом компиляторе был реализован ряд собственных расширений языка, ставших актуальными для того времени, таких как пользовательские типы данных. К сожалению, как всё, берущее своё начало с мейнфреймов, компилятор PL/I Workstation имел весьма значительную стоимость, начинавшуюся с нескольких тысяч долларов (образца 1990 года) за рабочее место. Понятно, что, имея альтернативой, например, Turbo Pascal за 50 долларов, просто так для пробы никто не стал бы покупать компилятор PL/I (да он и не находился в широком маркетинге), и популярности язык PL/I на рабочих станциях не получил. Также свои компиляторы, ещё менее известные, в то время выпустили для рабочих станций несколько других фирм, руководствуясь сходной с IBM ценовой политикой.
Рынок же мейнфреймов резко сократился, и последовавшие за PL/I (O) компилятор IBM PL/I for MVS & VM, а затем IBM Enterprise PL/I for z/OS, получили уже очень ограниченное применение.
Всё это время стандартизацией PL/I занимался отдельный комитет ANSI/ISO, ничем полезным себя не зарекомендовавший.
Перейдём к рассмотрению непосредственно некоторых свойств языка PL/I, подразумевая под ним преимущественно диалект PL/I (O).
Общая структура программы
Программа на языке PL/I состоит из операторов, каждый из которых заканчивается точкой с запятой, и представляет собой одну или несколько раздельно транслируемых процедур. Процедура может возвращать значение, тогда она рассматривается, как функция. Процедура может содержать внутри себя другие процедуры, и так далее, тогда такие процедуры транслируются совместно, а области видимости определённых в них объектов вкладываются друг в друга. Процедура может иметь опцию MAIN, тогда она вызывается в качестве главной при загрузке исполняемого кода. Процедура может иметь различные атрибуты, например, RECURSIVE (понятно), REENTRANT (допускает параллельные вызовы), REDUCIBLE (не имеет побочных эффектов), TASK (вызывается как независимая подзадача). Процедура может иметь различные опции, например, связанные с различием способов вызова в различных системах программирования, такие как FORTRAN или ASSEMBLER.
Минимальная исполняемая программа на PL/I:
empty: procedure options (main);
end empty;
Многие ключевые слова PL/I можно сокращать, например, вместо PROCEDURE писать PROC. Некоторые синтаксические элементы являются необязательными, например, вместо end empty; можно было бы написать просто end; Но лучше в данном случае указать имя, чтобы не ошибиться с несбалансированными операторными скобками.
Обычно в PL/I все параметры процедур передаются по ссылке. Чтобы защитить передаваемую переменную от возможной модификации, можно использовать синтаксический трюк, поставив её в скобки и превратив таким образом в выражение, переприсвоенное значение которого не используется. Например, вместо call (a); написать call ((a));
Тем не менее, в опциях параметров процедуры можно явно указать способ передачи BYVALUE или BYADDR. В основном это используется для компоновки с процедурами на других языках программирования.
Блоки
Операторы могут группироваться в блочную структуру двух видов: do-блоки и begin-блоки.
Do-блок представляет собой операторные скобки для группирования операторов для исполнения, а также может использоваться в качестве опаратора цикла. Он имеет множество различных форм, которые могут произвольно комбинироваться между собой. Семантика всех циклических форм строго определена через эквивалентные присваивания и goto.
Простейшая операторная скобка:
do;
end;
Простейший итеративный цикл:
do i = 1 to 100;
end;
Цикл с перечислением:
do i = 1, 3, 7, 9;
end;
Циклы с пред- и пост-условиями:
do while (a != 0);
end;
do until (a = 0);
end;
Цикл с произвольным изменением управляющей переменной:
do i = 1 repeat (i*2) to 128;
end;
Бесконечный цикл:
do forever;
end;
Цикл с управляющей переменной, такой, что сначала проверяется конечное значение, а затем переменная меняется:
do i = 1 upthru 32767;
end;
(заметим, что цикл do i = 1 to 32767 зациклится навсегда, так как в 16-разрядных числах нет значения, для которого выполнится i > 32767).
Всё это можно комбинировать произвольным образом:
do i = 1 to 100 by 2 while (a != 0) until (b = 3);
end;
Внутри do-блоков можно использовать операторы leave и iterate.
Вторым видом блоков являются begin-блоки. Они имеют вид:
begin;
end;
Помимо того, что begin-блок группирует внутри себя операторы для выполнения, begin-блок также локализует внутри себя области видимости описанных в нём объектов и области обработки исключительных ситуаций.
Описание переменных
PL/I имеет совершенно необозримую систему встроенных типов данных, характеризуемых бесчисленным количеством атрибутов, одновременно относимых к одному объекту. К счастью, для этих атрибутов действуют вполне разумные умолчания, позволяющие не задумываться о большинстве из них.
Переменные описываются при помощи оператора DECLARE (DCL), содержащего список переменных с их атрибутами. Оператор DECLARE может находиться в любом месте программы, но интерпретируется так, как если бы был записан в начале наиболее внутреннего begin-блока или процедуры, к которому относится.
Рассмотрим, например, числовую переменную. Она может быть с фиксированной (FIXED) или с плавающей (FLOAT) точкой, двоичной (BINARY) или десятичной (DECIMAL), иметь общее количество разрядов (например, 31) и количество разрядов после запятой (например, 0), быть знаковой (SIGNED) или беззнаковой (UNSIGNED), действительной (REAL) или комплексной (COMPLEX), распределяться в памяти статически (STATIC), автоматически (AUTOMATIC), в собственном стеке (CONTROLLED) или в собственной или общей куче (BASED), быть обычной (NORMAL) или внешне изменяемой (ABNORMAL), переменной (ASSIGNABLE) или константой (NONASSIGNABLE), в родной для машины кодировке (NATIVE) или в чужой (NONNATIVE), размещаться в памяти последовательно (CONNECTED) или как придётся (NONCONNECTED), иметь или не иметь начальное значение (INITIAL) и размерность (DIMENSION) и т.д.
Также числовые и символьные переменные могут задаваться шаблонами (PICTURE), как в Коболе, определяющими как их внутреннее представление, так и изображение на печати.
Аналогично, разные атрибуты имеют переменные других типов, в том числе очень много специфических атрибутов и опций имеют переменные типа файл.
Переменная может объявляться явно в операторе DECLARE или неявно, по факту использования. Атрибуты неявно объявленной переменной определяются по контексту её первого использования (например, если мы используем вывод в переменную, то это файл) или по имени (как в Фортране, переменные с именами, начинающимися с IJKLMN – целые, остальные вещественные).
Литеральные константы имеют атрибуты, соответствующие их написанию. Например, 1
имеет тип DECIMAL FIXED (1,0), 1.0
имеет тип DECIMAL FIXED (1,1), а 1e0
имеет тип DECIMAL FLOAT (6). Совершенно очевидно, что при таком богатстве атрибутов необходимы правила преобразования типов. В действительности, любые значения, для которых можно придумать хоть сколько-нибудь осмысленные правила преобразования, неявно преобразовываются друг к другу. Иногда эти правила не вполне очевидны (например, 1.1/5
= 0.0
по причине точности 1 цифра после запятой), но в целом разобраться несложно.
Строковые типы представлены символьными (CHARACTER) и битовыми (BIT) строками фиксированной и переменной (VARYING) длины. Максимальная длина строки ограничена 32 килобайтами, текущая длина строки переменной длины хранится в скрытом счётчике.
Как уже упоминалось выше, существуют управляющие типы переменных, такие как метка (LABEL), файл (FILE) и т.д.
Очень интересным управляющим типом является тип AREA, представляющим собой просто область памяти, а по существу мини-кучу. Внутри переменной типа AREA могут быть размещены адресуемые указателями переменные с классом памяти BASED. При этом используется относительная адресация, поэтому значение типа AREA со всеми размещёнными в нём структурами может быть, например, записано как целое в файл и потом прочитано из файла. Это позволяет избежать сериализации и десериализации структур данных.
Переменные могут объединяться в массивы произвольной размерности. По умолчанию массивы индексируются от 1, но при описании может быть указан произвольный нижний индекс. Массивы имеют гражданство первого класса, они могут присваиваться одним оператором, участвовать в арифметических операциях, от них могут браться сечения и т.д.
Переменные могут объединяться в структуры. Вложенность структур задаётся не придуманными позже операторными скобками, а как в Коболе, числами (внешний уровень имеет номер 1, а дальше уровни нумеруются произвольно), например:
declare
1 struct unaligned,
10 i binary fixed (15, 0),
10 x decimal float (16);
Оператор присваивания
Могут присваиваться по несколько переменных сразу:
a, b, c = 0;
Могут присваиваться массивы или их сечения целиком.
В левой части оператора присваивания могут использоваться так называемые псевдопеременные, представляющие собой, как сейчас бы сказали любители C++, функции над lvalue:
substr (str, 1, 1) = '*';
Ввод-вывод
PL/I имеет огромные возможности ввода-вывода, отчасти диктуемые разнообразием устройства наборов данных и методов доступа на мейнфреймах.
Все файлы делятся прежде всего на потокоориентированные (STREAM) и записеориентированные (RECORD), для работы с которыми существуют два набора операторов ввода-вывода. Те и другие файлы открываются и закрываются операторами OPEN и CLOSE.
Потокоориентированный ввод-вывод соответствует обычной для рабочих станций концепции файла как потока символов. Работа с ними осуществляется операторами GET и PUT, которые сами имеют достаточно разветвлённый синтаксис.
Оператор PUT LIST (выражение); осуществляет вывод значения выражения в свободном формате. Например:
put skip list ('Hello world!');
Ключевое слово skip вызывает перевод строки (из-за особенностей мейнфреймов общепринято переходить на новую строку до печати текста, а не после).
Оператор PUT EDIT (шаблон) (выражение); выводит выражение в соответствии с шаблоном, в точности как в Фортране, только с требованиями к синтаксису попроще:
put skip edit (a(20)) ('Hello, world');
Оператор PUT DATA (переменная); выводит имя и значение переменной. Например:
declare hello character varying (20);
hello = 'Hello, world!';
put skip data (hello);
вызовет примерно такую печать:
HELLO = 'Hello, world!';
По умолчанию вывод производится в системно определённый файл SYSPRINT, соответствующий системному выводу. С помощью конструкции FILE можно осуществлять вывод в другие файлы.
Все эти конструкции можно комбинировать, например:
put file (myfile) skip list ('Hello world!') skip edit (f(7,3), i(7)) (a, n) data (d);
Аналогично действует оператор потокоориентированного ввода GET.
Интересным случаем является оператор ввода без параметров:
get data;
В ответ на этот оператор из файла могут быть введены имена и значения любых переменных, область видимости которых включает данный оператор get, например:
A=10.3, N=5, HELLO='HELLO, WORLD!';
Для компилируемого языка, каковым является PL/I, реализация такого оператора достаточно накладна.
Записеориентированный ввод-вывод позволяет работать с записями файлов целиком, представляя их во внутренней форме. Записеориентированные файлы могут быть множества разных типов, например, с последовательным (SEQUENTIAL), прямым (DIRECT), ключевым (KEYED) доступом и ещё ряда видов, определяемых особенностями мейнфреймовских методов доступа (региональные разных типов и т.д.). Ввод-вывод осуществляется операторами READ и WRITE, которые на практике всегда имеют конструкцию FILE. Например:
read file (f) into (struct);
или
read file (f) into (struct) key (name);
Во втором случае осуществляется чтение структуры struct из файла с ключевым доступом, причём выбирается запись, значение ключа которой равно name. При этом используется мейнфреймовский индексно-последовательный метод доступа, который фактически представляет собой простейшую СУБД.
Можно при чтении использовать указатель. Например, так:
declare
1 struct unaligned based (p),
2 i binary fixed (15, 0),
2 x decimal float (16);
read file (f) set (p);
put skip data (struct);
При этом чтение будет осуществлено в буфер, предоставляемый ОС, указателю p будет присвоен адрес этого буфера, а переменная struct в нашей программе определена таким образом, что ссылается на значение структуры по адресу p. Таким образом мы работаем со структурой struct, избежав двойной буферизации ввода-вывода.
Атрибуты файлов весьма многочисленны и включают, например, такую экзотическую вещь, как атрибут BACKWARDS, позволяющий читать магнитную ленту задом-наперёд при её движении в обратном направлении.
Исключительные ситуации
PL/I был первым языком, в котором было предусмотрено управление исключительными ситуациями (CONDITION) на высоком уровне.
Программа на PL/I предусматривает возможность возникновения более чем десятка системных исключительных ситуаций (переполнения, ошибки ввода-вывода, завершения программы и т.д.), часть из которых по умолчанию разрешена, а часть запрещена, а также неограниченного количества пользовательских исключительных ситуаций. Любая ситуация может быть разрешена или запрещена префиксом перед простым оператором, begin-блоком или процедурой, например:
(nosubsctiptrange): put skip data (a(i));
Ситуация может быть принудительно вызвана оператором SIGNAL. Для ситуации может быть оператором ON определён обработчик, который относится к минимальному объемлющему begin-блоку или процедуре:
on finish put skip list ('Прощайте навеки!');
Обработчик можно отменить оператором REVERT.
Для отладки можно использовать ситуацию CHECK (переменная), которая возникает при изменении значения переменной.
В контексте обработчика через специальные функции доступна информация о характере и месте возникновения ситуации.
Параллельное выполнение программы
PL/I был первым языком высокого уровня, позволявшим программировать параллельные процессы. К сожалению, параллелизм в мейнфреймах был на уровне одного пользователя достаточно ограничен, и позволял исходно всего лишь порождать до 15 подзадач и выполнять асинхронный ввод-вывод. Эти возможности и были реализованы в языке.
Впоследствии в PL/I Workstation было реализовано также управление нитями, подходящее для ОС рабочих станций.
Препроцессор
Спецификация языка PL/I включает мощнейший препроцессор. Операторы препроцессора начинаются с символа процента. На уровне препроцессора можно определять переменные, выполнять присваивания, писать собственные процедуры и т.д. Фактически языык препроцессора представляет собой подмножество самого языка PL/I, причём вычислительно полное, так что при желании многие программы можно было бы написать собственно на языке препроцессора.
Компилятор PL/I по умолчанию не вызывает препроцессор, это делается двумя отдельными режимами компиляции – для полного препроцессирования или только для обработки операторов %INCLUDE, выполняющих вставку текстов (например, с определениями переменных). Хотя язык PL/I какое-то время был для автора основным языком программирования и интенсивно использовался им в коммерческой разработке, автору ни разу в жизни не довелось применить препроцессор, исключая %INCLUDE, так как вообще-то потребность в нём практически исключается богатством средств самого PL/I.
SQL
Малоизвестно, но исторически язык SQL разрабатывался Коддом именно как расширение PL/I. IBM SQL/DS, позже переименованная в DB2, позволяла записывать статические операторы SQL в программе на PL/I, и обрабатывать их в виде отдельного шага компиляции, выполняемого совместно компилятором PL/I и СУБД DB2. В результате компиляции получались два результата – объектный код программы и пакет исполняемого кода SQL в базе данных. Объектный код ссылался на пакет. Далее из объектного кода PL/I можно было построить либо загрузочный код пользовательского приложения, либо код хранимой процедуры DB2.
Операторы SQL оформлялись в программе, например, таким образом:
exec sql insert into table t values (:par1, :par2);
Не случайно типы данных SQL являются подмножеством типов данных PL/I.
Впоследствии использование статического SQL было расширено на языки Си, Фортран и некоторые другие, но там препроцессор SQL является отдельной от компилятора программой, поставляемой в составе СУБД и генерирующей файл на чистом включающем языке с библиотечными вызовами.
Вывод
К сожалению, язык PL/I оказался привязан к среде мейнфреймов IBM и мгновенно утратил свои позиции вместе с ней. Фирма IBM была не заинтересована в безвозмездном распространении языка на рабочих станциях, а программисты в своём большинстве не пользовались всеми разнообразными возможностями языка, изучали только его небольшое подмножество и без особого для себя ущерба переходили на более простые языки.
В настоящее время многие концепции языка PL/I возрождены в современных версиях Фортрана, но тот сам не сказать чтобы являлся очень популярным языком.
Автор: Вадим Румянцев