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

7cfe4f44ef47f773f0604252eafb7628.png

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

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

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

«Высокоуровневые» языки программирования начались с FORTRAN-а в 1957, за которым вскоре последовали LISP и Algol в 1960. Языков похожих на Фортран с его частыми метками в строках, коммон-блоками и отсутствием рекурсии (изначально) вы сейчас уже пожалуй не найдёте (хотя Бейсик в 1963 году немало его напоминал). Лисп в разных диалектах встречается и до сих пор — в отличие от прочих он был интерпретируемым (и позволял самомодификацию кода и т.п.) -, но его скобочный синтаксис всегда сперва удивляет. А вот Алгол — его очень легко перепутать с Паскалем — именно в нём появилась «блочная» структура кода, все эти begin-end (которые в С-подобных языках заменились на фигурные скобки). Фактически Алгол стал «дедушкой» всевозможных языков с «паскальным» и «сишным» синтаксисом. В 1963 году был придуман CPL (combined programming language) — в котором begin-end попытались заменить более короткими символьными последовательностями. Этот язык однако имел необычную судьбу — он был задуман довольно сложным, для научных вычислений в том числе — и так получилось что полноценный компилятор появился лишь в 1970. Гораздо более простой язык BCPL созданный на основе 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.

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

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

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

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

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

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

© Habrahabr.ru