Язык BCPL из которого получился C

в 6:14, , рубрики: история, Си, языки программирования

Готовясь к собеседованиям по Go я обратил внимание на то что среди его создателей Кен Томпсон - я смутно помнил что он также стоял у истоков языка C, но без подробностей. На самом деле было примерно так: Мартин Ричардс написал BCPL, Кен Томпсон переделал его в B повыкидывав "ненужное" и улучшив синтаксис, а Деннис Ритчи добавил разнообразие типов чтобы получился язык С который мы уже более-менее представляем.

И вот я решил заглянуть в BCPL - насколько он был похож и в чем отличался. Кратким обзором - сравнением с С я и хочу поделиться! Вы сможете и сами "пощупать" его при желании :)

Небольшая предыстория

"Высокоуровневые" языки программирования начались с FORTRAN-а в 1957, за которым вскоре последовали LISP и Algol в 1960. Языков похожих на Фортран с его частыми метками в строках, коммон-блоками и отсутствием рекурсии (изначально) вы сейчас уже пожалуй не найдёте (хотя Бейсик в 1963 году немало его напоминал). Лисп в разных диалектах встречается и до сих пор - в отличие от прочих он был интерпретируемым (и позволял самомодификацию кода и т.п.) - но его скобочный синтаксис всегда сперва удивляет. А вот Алгол - его очень легко перепутать с Паскалем - именно в нём появилась "блочная" структура кода, все эти begin-end (которые в С-подобных языках заменились на фигурные скобки). Фактически Алгол стал "дедушкой" всевозможных языков с "паскальным" и "сишным" синтаксисом.

В 1963 году был придуман CPL (combined programming language) - в котором begin-end попытались заменить более короткими символьными последовательностями. Этот язык однако имел необычную судьбу - он был задуман довольно сложным, для научных вычислений в том числе - и так получилось что полноценный компилятор появился лишь в 1970. Гораздо более простой язык BCPL созданный на основе CPL оказался готов раньше (1967)! Более того, был готов и язык B (1969) - упомянутое упрощение BCPL - а его "типизированная" версия под названием C появилась в 1972. Всё это привело к тому что сам CPL использовался мало и скоро практически исчез.

Мне кажется это добрым молодцам урок - придумывай проще, делай быстрее. Долгие проекты могут опоздать.

Название BCPL таким образом должно было означать что-то вроде "Basic CPL", в смысле простой или базовый - но поскольку про CPL уже начали забывать то неудивительно что Кен Томпсон для своей модификации повыкидывал не только ненужные фичи языка - но и ненужные буквы из названия.

Первый взгляд

get "LIBHDR"                // включаем заголовочный файл

let start() be $(           // вместо main - start, и скобки немного другие
    writes("Hi, People!")   // writes - Write String
$)

Программа "hello world" выглядит настолько похоже на привычную в С - мало что можно добавить. Точки с запятой в конце строки необязательны - только если нужно разделить пару стейтментов в одной строке. Скобки вокруг тела функции в данном примере можно было не ставить let start() be writes("...") - это сработает, ключевое слово "let" используется как для объявления переменных так и функций.

Посмотрим на программу вводящую и складывающую числа:

get "LIBHDR"

let start() be $(
    let a, b = 0, 0
    a := readn()
    b := readn()
    writen(a + b)
$)

Как было сказано let объявляет переменные - при этом инициализирующие значения обязательны, что не всегда удобно (в данном случае они все равно не нужны, но проинициализировать сразу чтением из readn() нельзя). Очевидно readn() считывает число а writen(...) печатает число в консоль. Обратите внимание на оператор присваивания (паскально-алгольный) в противовес знаку равенства при инициализации - напоминает ситуацию в Go...

Но главное - переменные не имеют типа! Это не потому что в языке динамические типы или duck typing - просто тип может быть только одним, хотя и в нескольких ипостасях:

  • либо это целое число, равное по размеру слову процессора (скажем, 32 бита)

  • либо это число - указатель, адрес памяти - на начало массива

  • в массиве же могут лежать текстовые данные, строка - обычно упакованные

  • и конечно указатель может указывать на функцию

Таким образом для всего этого используется один тип - вроде нашего int.

Вдогонку можно отметить что присваивание нескольких величин одним оператором возможно, например a, b := readn(), readn()- но это работает не так как в Питоне, а выполняется все равно по очереди - т.е. обменять две переменные так не получится.

Компилятор OBCPL

На случай (мало ли) если уже захотелось попробовать - в сети можно найти несколько реализаций BCPL и для современных машин - одна из по-видимому наиболее эффективных это OBCPL который может быть найден в нескольких чуть различающихся версиях, например https://github.com/SergeGris/BCPL-compiler - я взял отсюда, он легко компилируется под Linux / FreeBSD.

Онлайновых вариантов я не нашёл - поэтому добавил этот же компилятор на сайте CodeAbbey (мой сайт с задачками, теперь опенсорсный) - правда чтобы добраться до "запускалки" нужно залогиниться, но регистрация простая, не требует телефона или валидной почты.

Управляющие конструкции

При взгляде на набор всевозможных условий и циклов становится ясно почему Кен Томпсон решил это дело слегка упростить. Вот условные операторы, их три вместо одного:

  • if A then B - условный оператор без альтернативы

  • test A then B else C - условный оператор с альтернативой, использует другое ключевое слово (вместо if - test)

  • unless A then C - условный оператор "если-не"

Здесь отметим - скобки вокруг выражений (условия) не требуются. Слово then почти всегда можно пропустить. Почему понадобился unless? Дело в том что И/ИЛИ/НЕ в языке есть, но они только битовые! Зато впоследствии он перекочевал например в Perl.

Конечно B и C могут быть блоками из нескольких стейтментов, в скобках $( ... $) - как и в привычных нам языках.

А вот циклы. Их много:

  • while E do C и также until E do C - циклы "пока" и "пока-не" с пред-условием

  • C repeat - бесконечный цикл, как ни странно, ключевое слово после тела

  • C repeatwhile E и конечно C repeatuntil E - циклы с пост-условием (Е - оно по-прежнему не требует скобок)

  • for N = E1 to E2 by K do C - цикл "for" с локальной переменной N, причем "by K" - шаг цикла можно пропустить, он конечно равен 1 по умолчанию.

Как и с условиями, тело цикла С может быть написано в "долларовых" скобках, если в нём несколько действий - а слово DO опционально.

С циклами используются такжеbreak и loop - последний является аналогом continue.

Для досрочного возврата из функции используется знакомый return но если функция возвращает значение то он превращается в resultis - и сама функция в описании должна использовать знак равенства и ключевое слово valof вместо be - пример с факториалом выглядит вот так:

let fact(n) = valof $(
    resultis n = 0 -> 1, fact(n-1) * n
$)

Здесь использован тернарный оператор - он имеет слегка другую форму чем в Си A -> B, C - вычисляется B или C в зависимости от проверки A на равенство нулю. Вообще valof и resultis это конструкция которую можно использовать и просто как отдельный блок а не функцию - может быть довольно удобно!

Массивы и Указатели

get "LIBHDR"

let start() be $(
    let a = vec 9
    for i = 0 to 9 a!i := readn()
    for i = 9 to 0 by -1 $(
        writen(a!i)
        wrch('*N')
    $)
$)

Здесь мы объявляем массив с помощью ключевого слова vec - как видно, оно резервирует память на стеке среди прочих локальных переменных - под требуемое количество ячеек. Немного путает что указывается "последний индекс" а не общий размер (то есть, 9 вместо 10 для десяти ячеек).

Обращение к элементу массива - вместо квадратных скобок - с восклицательным знаком (не удивлюсь если многие клавиатуры тогда не имели ни фигурных ни квадратных скобок). То есть a!i - И-тый элемент массива А. Функцию wrch(...) мы ещё не видели - но она очевидно печатает символ (аналог putc) - специальные символы вместо привычного слеша обозначаются звёздочкой, в данном случае это перевод строки.

Указатели, как и в Си, близко перекликаются с массивами. Символ @ позволяет получить адрес переменной, а ! наоборот "дереференсит" указатель, например:

let a, b = 0, 0
a := 15
b := @a          // B теперь указывает на A
!b := 51         // по адресу лежащему в B запишем новое число
writen(a)        // конечно оно оказалось в A

У восклицательного знака таким образом две разновидности - если он "унарный" - перед именем переменной - то это "дереференс". А если он между двумя выражениями - как в случае с элементом массива - это на самом деле "синтаксический сахар":

a!i          берем из массива по индексу, как a[i] в C
!(a + i)     прибавляем индекс к указателю и дереференсим его, как *(a + i)

Эти две строчки идентичны - то же самое сделано и в си. И отсюда же следует что можно поменят их местами i!a - что сработает и в Си по крайней мере если предупреждения на этот счет выключены.

На этом же механизме реализованы и прототипы структур. Можно объявить константы и использовать их для дереференса:

let person.age, person.iq = 0, 1
let a = vec 10
a!person.age := 25
a!person.iq := 113

Заметьте что точка - это просто допустимый символ в идентификаторе, к созданию структуры он не относится. К сожалению в таком виде всё-таки нужно аккуратно следить чтобы поля одной структуры в массиве не пересеклись с полями следующей. Автор довольно подробно поясняет эту технику в своей книге.

Динамические массивы в базовый стандарт языка не входят, но присутствуют по словам автора почти в любой реализации. Например в OBCPL для них используются функции getvec и freevec (аналоги malloc и free).

Как вскользь упоминалось выше, указатели на функции используются легко и непринуждённо, в отличие от Си не приходится мудрить с декларацией типа:

    let blaha = 0
    blaha := writes
    blaha("muha")

Заключение

Мы много чего ещё не посмотрели - объявление констант, глобальных и статических переменных, работу со строками (которые записываются в массив "паскальным" способом, со счетчиком длины в начале), разнообразные функции для работы с потоками (файлами) - переключающие input и output по сути. Но мне кажется что для первого знакомства пока хватит :)

Язык оказался довольно удачным в том смысле что позволял писать и довольно низкоуровневые вещи, и обеспечивал хорошую переносимость высокоуровневых программ. В частности как я рассказывал ранее, на нём написан первый MUD - исходники можно найти на гитхабе (а поиграть - на british-legends.com)

Мы видим как много идей перешло в Си (и далее) с минимальными изменениями. В то же время впечатляет как много "возможностей для улучшения" оставил автор будущим разработчикам языков B и C.

Идея "одного типа" во времена 8 и 16-битных домашних компьютеров была неудобной - а сейчас например в программировании для ARM как будто опять звучит актуально.

Интересной и важной особенностью языка являлось то что он компилируется в три этапа (часто это три отдельных программы - в том числе в OBCPL):

  • сначала текст разбивается на токены из которых формируется структурное дерево лексем, представляющих программу

  • структурное дерево преобразуется в переносимый O-code - этакий мета-ассемблер из сравнительно небольшого числа операций

  • а уже только O-код на самом деле компилируется в нативный код

Этот подход был впоследствии массово использован и в других языках - получалось что для портирования языка на другую платформу достаточно переписать только третью часть (компиляцию O-кода).

Это кстати подробно описано автором в отдельной главе его книги (BCPL, the language and its compiler) - которая поэтому может быть актуальна для любителей изобретать новые языки даже сейчас.

Автор: RodionGork

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js