Интересно ли вам, что из себя представляли языки B и BCPL? Мне всегда было интересно, но как-то не подоворачивалось информации. А тут благодаря
https://vak.dreamwidth.org/1367620.html посмотрел на историческую веб-страничку ДМР (
http://cm.bell-labs.co/who/dmr/) и прочитал описания B (
http://cm.bell-labs.co/who/dmr/kbman.pdf) и BCPL (
http://cm.bell-labs.co/who/dmr/bcpl.pdf). Там же есть исходники двух ранних версий компилятора C (
http://cm.bell-labs.co/who/dmr/primevalC.html). Заодно поностальгировал про ранний интернет, где 1 МБ - был БОЛЬШОЙ файл и об этом предупреждали.
В-общем, B был практически как ранний С (т.е. без структур) без типов. Точнее с одним целым типом "слово", который был и целым и указателем (сдвиги всегда были логическими, то есть для них целые всегда были беззнаковыми). Доступ к байтам был через функции упаковки-запаковки.
Оператора for еще не было. Операторов break и continue не было, хотя break был в BCPL.
Операций && и || еще не было, вместо них использовались битовые & и |. Операции с присваиванием были как в раннем C, с = слева, то есть например не +=, а =+.
Между прочим, пробелы уже тогда были явной частью языка для различения операций, а без них бралось самое длинное имя операции, которое видно, то есть скажем "-----" - это "-- -- -", а не логически более осмысленное "-- - --".
Инциализация переменных была тоже как в раннем C, без знака =. Интересно, что в инициализации для взятия адреса переменной не нужно было использовать & как в других местах.
Умолчальным классом переменных был не автоматический, а статический (и для него даже не было слова static, а просто каждая не явно определенная переменная предполагалась по первому использованию статической), а для автоматической надо было писать auto. Extern назывался extrn.
В строках эскейпинг делался не через \, а через *, как в BCPL. Была определена последовательность *e, которая использовалась для конца файла, и также для конца строки (*0 была определена, но очевидно не использовалась для конца строки). Я подозреваю, что фактическое значение *e было такое же, как и *0, поскольку в BCPL на конце строки тоже был 0.
В символьных константах, как и в раннем C, и в BCPL, можно было писать два символа, которые упаковывались в двухбайтное слово.
Язык компилировался в шитый код (threaded code) и в описании даже приведен конкретный пример, как. Программа была последовательностью адресов функций без явного возврата, вместо возврата был переход на следующий адрес из программы через
jmp *(r3)+
Привет spamsink'у, который недавно обнаружил аналогичный код в другом месте. Это, видимо, было для тех времен типовым и модным решением.
Еще в описании есть полная реализация функции printf (масенькая) как она тогда была. А, вот еще, в отличие от C, и как в BCPL, можно было вызывать функцию с меньшим количеством аргументов, чем в декларации, но не с большим. Поэтому у printf написана куча неиспользуемых аргументов, от которых берется только адрес первого.
И еще более интересный момент: указатели (хотя тогда такого слова не было, а называли их lvalue) представлялись не в виде номера байта, а в виде номера слова. Поэтому арифметика на них была простая целочисленная, например ++ буквально увеличивал указатель на единицу, а сдвиг влево делался при разыменовании. И да, при взятии lvalue (т.е. адреса) переменной делался сдвиг (почему-то арифметический, а не логический) вправо.
Argc и argv еще не были разделены, в языке с единственным типом это не требовалось, число аргументов просто передавалось в первом слове argv.
Теперь про BCPL:
Семантически он почти эквивалентен B, но с чуть другим синтаксисом. Как сам Томпсон пишет в описании B, он "по сути BCPL с более богатым набором операций, но более бедным набором операторов". Подобно Алголу, BCPL был определен в виде логических лексем, для которых существовали варианты представлений (возможно это было связано с тем, что у разных машин тогда были разные сильно ограниченные наборы символов, и все выкручивались как могли, а для публикации в журналах использовали типографические красивости). Например, "(" была лексемой RBRA, а ")" - RKET. Поэтому неудивительно, что Ритчи с Томпсоном стали использовать более богатый набор символов из ASCII и получили B.
Богатый набор операторов сводился к тому, что для каждого логического оператора была альтернатива с обратным значением: if-unless, while-until (и варианты пост-условия для обоих), if был без else, а его вариант с else назывался test (видимо таким образом устранялась знаменитая сишная неоднозначность, которая тут была бы еще хуже, поскольку перевод строки был альтернативным вариантом точки с запятой). Был break (но не continue), и finish, который останавливал всю программу. Switchon переехал в B и C почти без изменений, но потерял "on" на конце.
Очень запутанно рассказано про lvalues и rvalues, и если не знать заранее, что они из себя представляют, то будет вообще непонятно. Возможно это потому, что более длинное описание было в языке CPL, а описание BCPL - это про что осталось от CPL после обрезания. Похоже на то, что понятия указателя и даже адреса тогда то ли не существовали, то ли в этой школе мысли считались неприличными, а более точными, приличными и умственными - lvalue (указатель/адрес) и rvalue (значение). Язык делался под машину с пословной адресацией, и потому естественным образом увеличение адреса на единицу давало следующее слово. Явным образом сказано, что аргументы функции идут на последовательных адресах, поэтому взяв адрес одной, можно его увеличивать и получать адрес следующих аргументов. Но в декларации функции надо было перечислить максимально возможное (а не минимально возможное как в C) количество аргументов.
Блок мог возвращать значение, для этого имелся специальный оператор resultis, подобный return (который выходил из подпрограммы). Можно было одной скобкой закрыть сразу несколько вложенных блоков, если пометить открывающую скобку, и потом использовать ту же метку у закрывающей скобки, все вложенные в него блоки тоже закрывались. Присваивание было групповое, можно было написать
Var1, Var2, Var3 := Val1, Val2, Val3
но функция не могла возвращать сразу несколько значений.
Аргументы функции писались в квадратных скобках, а массив (вектор) индексировался как звездочка и квадратная скобка, например array*[index]. Впрочем, Ритчи пишет, что в их версии BCPL они быстро поменяли квадратные скобки для функций на круглые. То есть, пардон, агрументы не только функций, но и подпрограмм, они различались, и функция определялась как выражение (но поскольку блок мог быть выражением и вернуть значение, то могли быть произвольной сложности), а подпрограмма как блок.
Комментарии были через //! То есть, оказывается, что в вопросе комментариев C++ прошел полный круг.
Имена переменных или начинались с большой буквы или были одной маленькой буквой, а все многобуквенные слова из маленьких букв были зарезервированы под служебные. Переменные классифицировались как static и dynamic, и по умолчанию переменные были динамическими (это исчезло в B), а статические требовалось объявлять или как global, или как метку(!) на операторе присваивания внутри функции. Имена функций и подпрограмм тоже считались переменными (это, видимо, алголоподобность), всегда статическими (но можно было их присваивать как lvalue, то есть указатель, в другие переменные).
Поскольку язык придумывался для пословных машин, символьные константы существовали отдельно от строковых, но могли содержать несколько символов, которые упаковывались в слово "выравненными вправо". То есть, я так понимаю, что в общем предполагалась паковка байт MSB first, но самый правый символ в константе попадал в самые младшие биты, а слева заполнялись нули (и исходя из того, что первая реализация была на машине IBM, это неудивительно и соответствует IBMным традициям). Доступ к символам шел через функции распаковки в слова.
Строковые константы имели одновременно длину в первом символе перед строкой и завершающий код 0 в символе после строки.
Были символичесие константы И (ограниченный) препроцессор, которые пропали в B.
P.S. У меня создается впечатление, что ключевым моментом перехода от B к C стал переход от шитого кода к непосредственной кодогенерации. А в остальном язык просто плавно эволюционировал по мере того как они добавляли фичи к компилятору.
P.P.S. Современное применение threaded code к асинхронному программированию:
https://babkin-cep.blogspot.com/2025/02/asynchronous-programming-9-composition.html (текст делается более понятным, если его читать с первой части)