Новый язык программирования своими руками и головой

от автора

Привет, Хабр! Сразу к делу. В данный момент я читаю «Книгу дракона» и занимаюсь разработкой компилятора под свой язык программирования, названный Лоло (в честь пингвинёнка из советско-японского мультфильма). Планирую закончить в течение года, если ничто не помешает. Параллельно буду выкладывать интересные выдержки из опыта трансляции, построения промежуточного кода, оптимизации итд., ну а сегодня просто познакомлю вас с языком. Присаживайтесь и поехали.

Язык компилируемый, императивный, не объектно-ориентированный, семантика нагло списана с Си и дополнена множеством полезных фич. С них и начнём.

Семантические модификации

Безопасные указатели

Возможно, вы сейчас подумали об «умных» указателях из Rust, но это не они. В моём языке безопасность обращения к памяти обеспечивается двумя идиомами. Первое: отсутствие операции разыменования указателей. Вместо неё при обращении к декларированному указателю происходит обращение к самому объекту, на который он ссылается. То есть, можно и нужно писать так:

int # pointer ~~ new int(5) int variable ~ pointer + 7

В переменной variable теперь содержится число 12. Сейчас вы видите незнакомый синтаксис и, возможно, немного недоумеваете, но я всё объясню по ходу статьи. Вторая идиома: отсутствие операций над указателями. Опять же: все операции при обращении к указателям, включая присваивание, инкремент и декремент производятся над объектами. Единственная операция, касающаяся непосредственно указателя — присвоение по адресу, или, как я его называю, отождествление. В примере кода, приведённом выше, в первой строке именно оно — отождествление. Любой указатель может быть поставлен на адрес только уже выделенной области памяти, какой и является возвращаемая операцией new. Можно так же поставить указатель на адрес другой переменной, выделенной хоть в куче, хоть в стеке. Вот пример:

int variable ~ 5 int # pointer ~~ variable

Здесь «~» — обычная операция присваивания. Ещё можно отождествлять указатели с особым указателем null. Он выступает как указатель, ссылающийся на нулевой адрес. После отождествления операции сравнения и сравнения на тождественность (одинаковые адреса) с null будут выдавать истину:

int # pointer ~~ null if (pointer = null) nop  ;; true if (pointer == nul) nop  ;; true

Здесь «=» — сравнение значений, «==» — сравнение по адресам, «nop» — пустая операция, а после «;;» — комментарий. И да, null — единственный указатель, операции с которым возможны без проверки совместимости типов.

Таким образом, указатели можно назначать только на выделенные области памяти или null и нельзя перемещать куда попало. Впрочем, от ошибки segmentation fault данные меры не защищают полностью. Чтобы её получить, достаточно выполнить следующие действия:

int # pointer1 ~~ new int(5) int # pointer2 ~~ pointer1 delete pointer1 int variable ~ pointer2  ;; segmentation fault!

Думаю, здесь всё понятно. Но допустить такую ошибку можно только специально, и то, сильно постаравшись. Ведь операция delete выполняет то же самое, что сборщик мусора, только менее безопасно. Кстати, о нем…

Сборщик мусора

Сборщик мусора — он и в Лоло сборщик. Наверное, не нужно объяснять, что это такое. Скажу только, что его можно отключить специальной опцией при компиляции. Протестировали программу со сборщиком, всё работает как надо — можно ввести опцию и попробовать оптимизировать программу с помощью ручного управления памятью.

Встроенные массивы

Хоть я и сказал, что семантика языка списана с Си, отличия довольно существенные. Здесь массивы ≠ указатели. У массивов свой синтаксис и безопасная адресация. Нет, не с проверкой диапазона. С ними в принципе сложно получить runtime-ошибку. Всё потому, что каждый массив хранит в переменной size длину, как в Java, и при каждой индексации от индекса… находится остаток от деления на этот size! Глупое решение, на первый взгляд, пока мы не посмотрим на отрицательные индексы. При нахождении остатка от деления -1 на длину массива получится число, равное size-1, то есть, самый конечный элемент. Таким манёвром мы можем обращаться по индексам не только от начала, но и от конца массива. Ещё один трюк — приведение любого примитивного типа к массиву byte[]. А как же всё таки получить runtime-ошибку, спросите вы? Оставлю вам этот вопрос самим как лёгкую загадку.

Ссылки

Не знаю точно, включает ли текущий стандарт Си ссылки, но в Лоло они точно будут. Пожалуй, отсутствие ссылок в ранних версиях Си — одна из основных причин указателей на указатели. Они нужны для передачи аргументов по адресу, для возвращения значений из функций без копирования. Указатели и массивы тоже можно будет передавать по ссылке (так как при передаче по значению массивы будут полностью копироваться, а поставленные на новое место операцией ~~ указатели не сохранят его).

Многопоточность

Всё прекрасней и прекрасней. Я уже влюблён в свой язык. Очередной его конёк — многопоточность. Честно говоря, я до конца не решил, какими инструментами она будет обеспечиваться. Скорее всего, ключевым словом synchronized со всеми свойствами аля-Java и, возможно, ключевым словом concurrent перед не-встраиваемыми (inline) функциями, которое означает «запускать эти функции в параллельных потоках».

Встроенные строки

Именно строки, а не строковые литералы, как в C++. У каждой строки будет своя длина, индексация будет происходить с нахождением остатка. В целом, строки в Лоло сильно похожи на символьные массивы, за тем исключением, что у массивов нет операций конкатенации через «+», мультипликации через «*» и сравнения через «<» и «>». И раз мы заговорили о строках, надо упомянуть и символы. Символы в Лоло — не числа, как в C++. И содержат не один байт, а 4 для DKOTI-символов и 6 для UTF-символов. Про DKOTI расскажу в следующий раз, а пока просто знайте, что Лоло поддерживает символы и строки в двух кодировках. И да, свойство длины можно брать даже из констант:

int len ~ "Hello, world!".length  ;; len = 13

Логический тип с тремя значениями

В подавляющем большинстве языков программирования, имеющих логический тип данных, используется двоичная логика. Но в Лоло она будет тернарной, а точнее, нечёткой тернарной. Три значения: true — истина, false — ложь и none — ничто. Пока я не нашёл в самом языке операций, возвращающих none, но зато помню много примеров из практики, когда очень пригодились бы флаги с тремя значениями. Приходилось использовать перечисления или целочисленный тип. Больше не придётся. Вот только название данного типа не могу выбрать. Самое банальное — «logical», но слишком длинно. Ещё варианты — «luk» в честь Яна Лукасевича, «brus» в честь Н. П. Бруснецова и «trit», но строго говоря, тритом данный тип не является. В общем, опрос в конце статьи.

Списки инициализации структур и списков

Если после декларирования структурной переменной поставить знак ~ и открыть квадратные скобки, можно задать значения её полей по очереди или в виде словаря. Если провести такую процедуру с массивом, можно задать значения его ячеек, только уже без словаря. Тут особо рассказывать нечего, просто посмотрите код:

struct {     int i;     real r;     str s; } variable ~ [ i: 5, r: 3.14, s: "Hello!" ] int[5] arr ~ [ 1, 2, 3, 4, 5 ]

Возврат нескольких значений из функций

Прямо как в Go! Можно написать несколько имён переменных через запятую и присвоить им всем сразу значения, возвращаемые из функции:

int, real function() {     return 5, 3.14 } byte § {     int i; real r     i, r ~ function }

Модули вместо заголовков

Тут всё ясно. Вместо Си-шных заголовков — модули из Java.

for (auto item: array)

Опять родная Java. Раз у нас есть массивы с длиной, грех не воспользоваться выражением for each.

Оператор выбора не только для int

Не знаю как вас, а меня в Си и C++ жутко бесит отсутствие возможности использовать операцию switch-case для не-целочисленных переменных. Да и синтаксис тоже бесит. Вот в Паскале — другое дело. А теперь и в Лоло:

case variable {     "hello", "HELLO": nop     "world": {         nop; nop     }     "WORLD": nop }

Операторы возведения в степень и деления нацело

А это уже из Python.

real r ~ 3.14 ** 2 int i ~ r // 3

Кортежи параметров функций

Помните, что в Лоло запрещены все операции с указателями, кроме отождествления? А теперь давайте вспомним, как получать доступ к параметрам функции из списков параметров переменной длины. Нужно объявить указатель на первый элемент, а затем инкрементировать, пока проверка на истиность возвращает true. В Лоло инкрементировать нельзя. Но ничего страшного. Ведь список параметров здесь представлен в виде кортежа фиксированной (зависящей от вызова) длины, с безопасным, как у массивов, индексированием. Имя его — «?» Проверка типов выполняется только для установленных в определении функции параметров. Остальные («многоточные») параметры приводятся к любому типу, и при неловком движении их поведение не определено. Но всё равно такой кортеж гораздо безопаснее и удобнее, чем макросы в Си.

void function(...) {     if (?.size > 1) {         int i ~ ?[0]         real r ~ ?[1]     } }

Числовые интервалы

И ещё один персонаж — семейство типов интервалов (range, urange, lrange итд.). Они задаются двумя целыми числами через две точки (..) и могут вырезать из массива массив, из строки строку, в общем, полезная штука, я считаю.

int[5] arr ~ [ 1, 2, 3, 4, 5 ] int[3] subarr = arr[1..3]  ;; [ 2, 3, 4 ]

Оператор in

Из паскаля. Работает со строками, массивами, кортежем? и диапазонами.

int[5] arr ~ [ 1, 2, 3, 4, 5 ] if (4 in arr) nop

Словарь параметров функций

Честно говоря, запутался уже, как эта штука правильно называется, с помощью неё можно задавать напрямую аргументы не-чистых (pure) функций:

int pos = str_find(string, npos: -1)

Параметры по умолчанию

Из C++. Тут даже пример приводить не нужно, и так всё ясно.

Исключения

Ну и куда же без них?

try {     raise SEGMENTATION_FAULT_EXCEPTION } except (Exception e) {     print(e.rus) }

Отсутствие безусловного перехода

Потому что в 2019 году использовать оператор GOTO смерти подобно.

Синтаксис

Ну и немного расскажу про синтаксис. Как вы заметили, отмелась точка с запятой. Современные языки программирования прекрасно обходятся без этого источника ошибок. Примеры — Python, Kotlin. Оператор «стрелка» (->) объединена с оператором «точка». При вызове функций без аргументов скобки не обязательны. Строки приводятся в числа и наоборот. Логические и побитовые операторы объединены. Есть модификаторы функций для табуляризации. Вложенные функции. type_of. А самое главное — мультиязычность. Да, я собираюсь дублировать ключевые слова, свойства строк и массивов и все идентификаторы стандартной библиотеки на всех языках международного общения, а именно: английском, русском, японском, китайском, испанском, португальском, арабском, французком, немецком и латыни.

На самом деле, всё вышеописанное не включает и половины возможностей Лоло. Я просто не могу сразу вспомнить всех его фич. Буду дописывать по мере готовности компилятора.


ссылка на оригинал статьи https://habr.com/ru/post/460283/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *