Как стать автором
Обновить

Как я 12 лет создавал свой ЯП и компилятор к нему

Время на прочтение22 мин
Количество просмотров50K


События истории рассказываются в хронологическом порядке, если вам кажется, что какое либо событие не относится к делу, то скорее всего — это не так, просто момент в котором будет понятно почему я упомянул это событие, будет позднее. Создаваемый мной язык, со временем менялся, если вы читаете и понимаете, что язык не очень и дальше можно не читать, то все же рекомендую дочитать до конца, возможно ваше мнение изменится. Изображение к статье не отражает главных особенностей языка, но в языке действительно нет ссылок.

Здравствуй, читатель! Меня зовут Александр, родился я в небольшом городке (меньше 10000 человек) в Беларуси. Моя семья была бедной, игрушек крайне мало, про компьютер и какие либо приставки вообще можно не заикаться. Не смотря на то, что семья была бедной, у матери были не бедные родственники, которые иногда дарили нам какие либо не дешевые вещи. И вот однажды (где то в 2001 году) эти самые родственники, дарят нам компьютер «Байт»(советский аналог ZX Spectrum 48k). Радости моей не было предела! Сразу же я начал, запускать на нем игры. Игры на этом компьютере загружались с обычных аудиокассет с магнитной лентой. Загрузка одной игры длилась примерно 5 минут и с не малой вероятностью, могла прекратиться из-за некачественного сигнала. Чтобы увеличить вероятность успешной загрузки, мне приходилось протирать спиртом и регулировать положение считывающей головки магнитофона. Весь этот шаманизм при загрузке, длительность загрузки и невозможность сохраняться в играх, привели к тому, что постепенно я начал терять интерес к играм. Но вместе с «Байт»-ом мне также подарили книгу, по работе с этим компьютером. Я решил прочитать эту книгу, чтобы узнать больше о возможностях «Байт»-а. В книге оказался учебник по встроенному в «Байт» языку программирования «Бэйсик».

Я просто взял и переписал первую попавшуюся программу из книги в компьютер, а затем запустил. Когда я увидел на экране круг, я просто обомлел — это были непередаваемые ощущения. Придя в себя, я начал менять различные числа в программе и после запуска фигура изменяла свою форму, я менял числа и запускал программу снова и снова, было ощущение, что я попал в другую вселенную и начал её изучать. Прочитав всё, что было в этой книге по бэйсику, я начал создавать свои собственные программы. Вначале писал их на листочке, компилировал и запускал в голове, а затем переносил это на компьютер и смотрел, что получится. После того как я сам без учебника написал свою первую программу, я понял — это моё, это то чем я хочу в жизни заниматься. Я стал целыми днями писать на листочке различные программы (включая игры).

Примерно через год, после событий описанных выше, у меня в школе появляется информатика. Со временем на уроках нас начинают учить программированию в программе «ИнтАл». На уроках мы программировали мало, так как было много других материалов не связанных с программированием, а также работали за компьютерами по очереди (компьютеров было меньше, чем учеников). Но в нашей школе был факультатив по информатике и я решил туда записаться, с надеждой, что на факультативе мне дадут больше программировать. На факультативе мы начали изучать Turbo Pascal, с первого момента мне язык очень понравился. Там были записи(они же структуры), юниты(они же модули и библиотеки), процедуры и функции. Это был какой то новый прекрасный мир, прекраснее чем все, что я знал до этого. Начав изучать паскаль, я окончательно решил, что хочу стать программистом.

Закончив 9 классов школы, у меня появился выбор, пойти в какой либо колледж, либо пойти в 10 класс. Я решил пойти в лицей, по специальности оператор ЭВМ с углубленным изучением физики, математики, астрономии и информатики. Многие люди говорили, что если я отучусь по этой специальности, то мне будет проще поступить в ВУЗ на программиста. На решение вступительного экзамена давалось три часа, я использовал только 30 минут, из которых 10 я проверял, все ли правильно. Поступил! В лицее я продолжил изучать Pascal, но также начал изучать Photoshop, Corel Draw, Excel, командную строку Windows и .bat файлы.

Узнав, что я поступил, те самые родственники которые подарили мне «Байт», подарили мне новый компьютер со следующими характеристиками: одно-ядерный Intel Celeron 800 Mhz, 64 Mb оперативной памяти, 14 Gb жёсткий диск, встроенное видео с разрешением 640 x 480 и 16 цветов. Родители скинулись и сделали апгрейд, купив 64 Mb оперативной памяти и видео карту Matrox Milenium g400 16 Mb, c поддержкой 16 миллионов цветов, разрешение стало 800 x 600 (ограничение монитора). В то время доступа в интернет почти не у кого не было, вся информация в основном бралась с дисков, которые продавались в магазинах.

Один из моих друзей купил несколько дисков по программированию на языке Delphi, на дисках был Borland Delphi 7, куча учебников, куча компонентов и программа с большим количеством вопросов по Delphi и с ответами на эти вопросы (эдакий офлайн StackOverflow на минималках).

Все было настолько просто и удобно, что программы писались одна за другой. Так же в библиотеке лицея был учебник по программированию на языке C. Прочитав её, я понял, что мне C совершенно не нравится, Pascal и тем более Delphi мне нравились в разы больше. C сложнее и не так удобен, единственное, что мне в нем понравилось — вместо begin end используются фигурные скобки. Но не нужно думать, что Pascal был для меня идеальным языком, со временем многие вещи в языке стали меня раздражать(помимо begin end). Например мне хотелось, чтобы строки и массивы были структурами со следующими полями: указатель на область памяти с данными, количество элементов, максимальное количество элементов которое может вместить область памяти на которую указывает указатель, чтобы несколько массивов или строк могли указывать на одну и туже область памяти и можно было создавать массив из куска другого массива не копируя элементы. Как позже выяснилось, такие массивы как я хотел, называются слайсами. Мне тогда захотелось написать транслятор который переводит из такого изменённого паскаля в обычный, но я отказался от этой идеи.

Тот самый друг который познакомил меня с Delphi, сказал, что некая компания бесплатно рассылает диски с какими-то Ubuntu и Kubuntu. Я даже не знал, что это такое, но он бесплатно дал мне 2 диска. Ubuntu у меня не запустилась, а вот Kubuntu запустилась и очень хорошо работала. Я был поражён — оказывается кроме Windows и dos есть, что-то ещё. Очень хотелось поставить Kubuntu на компьютер и по изучать его, но 14 Gb диск был категорически против. К тому же к этому времени у меня уже появился интернет (5 Kb/s), а модем в Kubuntu не работал. Поэтому диск с Kubuntu был закинут на полку. Неожиданно все тот же друг, начал бредить каким то ассемблером, все показывал мне какие то программы на нём, но у меня они вызывали лишь улыбку, поскольку 50 строк ассемблера заменялись одной строкой Delphi, но друг всё же уговорил меня попробовать и дал диск с FASM и учебниками по ассемблеру. Ассемблер мне не понравился, хотя макросы в FASM — классная штука.

И вот я закончил лицей, пришло время поступать в ВУЗ. Но незадолго до окончания лицея, я начал подрабатывать в интернете. В интернете есть куча мошенников которые, притворяются успешными предпринимателями и предлагают доверчивым пользователям следующую сделку — «Дайте нам хотя бы доллар, а через 2 недели мы вернем вам 2 доллара». Если этим мошенникам дать доллар, то как ни странно, через 2 недели они действительно возвращают 2 доллара, но если дать 10 то через 2 недели, будет указано, что у меня на счету 20 долларов, но снять их будет невозможно. Я создавал множество почтовых ящиков и используя различные почтовые ящики и меняя ip адрес (у меня был динамический ip), регистрировал на сайтах мошенников множество аккаунтов, каждый раз кладя на них 1 доллар. Учитывая то, что я таким образом уже немного зарабатывал, меня стала посещать мысль — «А может мне не идти в ВУЗ? Программировать в своё удовольствие я могу и без ВУЗ'а.». К этой мысли меня также склоняло и то, что в интернете часто писали, что программисту вышка не нужна. В то время как я размышлял — «поступать или не поступать?», жизнь за меня решила сама. Мой отец зарегистрировался на сайте знакомств, нашёл себе новую любовь и кинул меня с матерью. Понимая, что мать одна не сможет меня содержать, пока я буду учиться, я не стал поступать в ВУЗ. После лицея я обязан был отработать год на каком либо предприятии. Я устроился по специальности в продуктовый магазин, занимался выпиской ТТН и ввод пришедшего в магазин товара в компьютер. Немного поработав в магазине, я написал программу которая анализирует базу данных продуктов в магазине и ищет потенциальные проблемы. После этого в компании, которая владела магазином, предложили перевестись к ним на должность программиста, но узнав, что у меня нет высшего образования — передумали. И я остался дальше работать в магазине. Зарплата у меня была не плохая, а вот доход от мошенников был очень скромным. Низкими доходы были потому, что регистрация аккаунтов занимала много времени, а в Беларуси было очень тяжело работать с интернет деньгами. Liberty Reserve — интернет деньги, работу которых обеспечивал банк в Коста-Рике, который не выдавал информацию о своих клиентах правительству, из-за чего большинство мошенников и использовали Liberty Reserve.

Покупать эти деньги в Беларуси было особо затруднительно, так как в автоматическом обменники приходилось платить в семь раз дороже(если память не изменяет), а покупать у людей было рискованным занятием. Я перестал обманывать мошенников и получал доход, только от работы. Отработав год, решил поискать, что нибудь по лучше. У меня была на руках не плохая сумма денег и я решил обновить свой компьютер. Характеристики нового ПК: AMD Athlon 64 x2 2600 (с поддержкой SSE2 инструкций, которых не было в предыдущем процессоре. Это важно!), 1 Gb оперативной памяти, 80 Gb жёсткий диск. Вспомнив о том, что я хотел по изучать Kubuntu но у меня был слишком маленькие жёсткий диск, я решил поставить Linux и Windows одновременно, поскольку с диском у меня проблем уже не было. Сходив в магазин компьютерных дисков, мною был приобретен диск с openSUSE 10.2. Моей новой работой стала починка компьютеров в одной из компаний моего города, с испытательным сроком в один месяц. Начальник этой компании, а также его жена разрабатывали некое бухгалтерское ПО, узнав, что я увлекаюсь программированием, они предложили присоединится за процент от будущих продаж, но поскольку разработку они вели на Visual FoxPro и SQL, мне необходимо было выучить эти языки. Я согласился. Через месяц оказалось, что они и не собирались меня брать на работу, а нужен я им был, чтобы подменить ушедшего в отпуск сотрудника, когда он вернулся, они сказали, что я им больше не нужен, но все же хотели, чтобы я им помог с разработкой, я разумеется отказался и начал искать новую работу. Несколько месяцев безуспешных поисков привели к тому, что я пошёл на стройку подсобным рабочим.

Зарплата у подсобного рабочего была в 3.5 раза меньше чем у оператора ЭВМ в магазине, но надо сказать, что и работа на порядок проще(я был крепким парнем). В выходные дни, используя FASM, я начал изучать эти новые для меня SSE2 инструкции. Поэкспериментировав я понял, что в некоторых задачах, эти инструкции могут значительно увеличить производительность. Мне стало интересно, как разработчики ПО встраивают в свои приложения SSE2 инструкции, ведь если все задачи в которых имеет смысл использовать SSE2, будут решены с их использованием — то программа не будет запускаться на компьютерах без поддержки SSE2, но если их не использовать — то программа будет работать медленнее. Конечно можно в начале программы узнать, есть ли у процессора поддержка SSE2, и в зависимость от результата выполнять разный код, но в таком случае увеличивается сложность разработки и тестирования, а также увеличенное потребление оперативной памяти и кэша процессора. Проанализировав несколько программ, я увидел, что большинство программ не использует SSE2. И тут я задался вопросом «А почему компиляторы не компилируют в ассемблер с макросами, а на компьютере конечного пользователя некая утилита не заполнит необходимые для макросов константы и только после этого создаётся бинарник?». И я, вспомнив про видоизмененный паскаль, который я придумал в лицее, решил написать такой компилятор. Так же мной было принято решение, добавить в язык дженерики и язык стал выглядеть примерно так:

type Point(a){
    x, y: a;
}

type Line(a){
    a, b: Point(a);
}

function createLine(a, b: Point(a)) Line(a){
    result.a = a;
    result.b = b;
}

Со временем у меня появился интернет с нормальной скоростью и я полностью отказался от Windows в пользу Linux(было много дисков с программами для Windows, а качать их аналоги для Linux с медленным интернетом проблематично). На тот момент у меня был дистрибутив Ubuntu 8.10, но я активно интересовался и другими дистрибутивами. В какой то момент, я решил попробовать дистрибутив под названием Gentoo. Разбираясь как устанавливается Gentoo я узнал про use-флаги и сразу понял, что их поддержку нужно добавить в свой язык. То есть, при установке программы написанной на моём языке, утилита которая добавляет константы в ассемблерный код, будет также спрашивать значение констант которые определил программист при разработке и в зависимость от этих констант, макросы будут вставлять различный код. Почти закончив разработку компилятора, я из форумов узнаю о каком-то LLVM, в котором есть некий ассемблероподобный язык LLVM IR. Изучив немного, что это и с чем его едят, я решил компилировать не в FASM, а в LLVM IR, поскольку он умеет оптимизировать код, а мой компилятор практически ничего не оптимизировал, не говоря о том, что разработчики LLVM со временем наверняка будут добавлять новые оптимизации, а это значит, что мне не надо будет этого делать и я смогу сосредоточиться на других вопросах. И я начал переписывать свой компилятор. В LLVM IR помимо инструкций, были также параметры функций и параметры аргументов функций, например можно было указать, что аргумент функции, который является ссылкой, не будет скопирован. И я решил добавить такие параметры, только указывать их нужно было не при объявлении функции, а при вызове и в отличии от LLVM IR, эти параметры носили не информативный характер, для оптимизации, а были требованием поведения. Пример:

function inc(a: ^Integer) Integer{
    result = a^ + 1;
}

procedure foo(){
    var
        a: ^Integer;
        b: Integer;
    ...
    //некий код
    ...
    b = inc(ro nocp a) //если бы функция inc изменяла значение по ссылке или копировала ссылку, то код бы не компилировался
}

Со временем я уехал в Минск и устроился грузчиком, зарплата была очень хорошей (800 $/месяц), хотя времени на разработку стало меньше. Читая форумы, я стал очень часто натыкаться на упоминания языка Haskell, упоминания обычно сопровождались фразами вроде: «взрывает мозг», «очень необычно», а также упоминалось, что там есть какие-то монады, которые тяжело понять. Я не выдержал и решил изучить этот язык. Скачав целую кучу учебников по Haskell и начал учить. Взял первый учебник, написано очень мутно — удалил, взял второй, читаю и испытываю ощущение, будто все страницы перетасованы — удалил, беру третью, книга написана толково, но она как будто рассчитана на изучение через примеры(а я люблю изучить теорию, а затем подкрепить её практикой) — удалил, дело доходит до четвёртой, написано толково, изучается через теорию — похоже то, что надо. Книга называлась «Изучай Haskell во имя добра». Во время прочтения у меня не возникло ни одного вопроса, никаких проблем с монадами у меня так же не возникло. Мне настолько понравилась книга, что я пошёл и купил её бумажный экземпляр. Прочитав до конца книгу я понял, что все языки которые я видел до этого, попросту меркнут на фоне Haskell, это просто какой-то новый, недостижимый для других языков уровень. Я решил изучить Haskell глубже, прошёл курсы по Haskell(а заодно по основам статистики и языку Python), начал смотреть лекции по функциональному программированию и лямбда исчислению Чёрча.

Очень хотел изучить теорию категорий, но не нашёл никаких материалов объясняющих эту теорию простым и понятным языком. Я писал на Haskell всякие мелкие утилиты. В какой-то момент я наткнулся на вакансию Junior Haskell Developer прочитав требования я понял, что подхожу под все требования и решил попробовать. Написал резюме и откликнулся на вакансию. Думая, что мне пришлют для проверки навыков тестовое задание, я отрыл IDE, firefox + google и стал ждать ответа, ответ пришёл примерно через полтора часа где меня вежливо послали. Данная вакансия висела на сайте ещё год. Haskell стал моим основным языком и я начал писать на нём всё подряд.

Со временем я понял, что Haskell очень хорош, только если использовать декларативное программирование, но при таком раскладе программы получаются медленными и потребляющими чрезмерное количество памяти, на Haskell можно писать эффективно, но в этом случае приходится использовать императивный подход, а писать на Haskell императивно — сомнительное удовольствие! Я забросил Haskell и продолжил разработку своего компилятора, но решил добавить в свой язык функции высшего порядка и убрать фигурные скобки, а для структурирования кода использовать отступы (как в Haskell). Язык стал выглядеть так:

function lineLength(line: Line) Real
    result = sqrt(sqr(line.a.x - line.b.x) + sqr(line.a.y - line.b.y))

В какой-то момент случился кризис и мои 800$ превратились в 400$, аренда комнаты 120$ + дорога на работу и с работы + коммуналка и с зарплаты остаётся примерно 250$. А зачем мне жить в арендной комнате и получать 250$, если я могу жить в нормальной квартире и получать примерно те же деньги у себя в городе? Я решил уволиться. После увольнения у меня оставалась некоторая сумма денег и подумав, я решил — сниму жильё в любом населенном пункте Беларуси и там, нигде не работая и не на что не отвлекаясь, допишу компилятор. Я нашёл в одном городке целый дом за 35$ в месяц. Дом не большой, старый, туалет на улице, нет душа и ванной комнаты. Но ценник был очень соблазнительный, да и хозяйка сказала, то могу выедать всё, что есть в огороде, а там было много овощей и фруктов, из-за чего я мог сильно сэкономить на еде. Я согласился и заплатил аренду за 2 месяца. На перевозку всех вещей у меня ушёл целый день и заселился я только ночью, из-за чего придя домой я сразу лёг на кровать и уснул. Проснувшись, я увидел, что хлеб, который я оставил в пакете на столе, погрызен крысами, «Ну и чёрт с ним, что крысы в доме. Буду прятать продукты в металлический холодильник» — подумал я в тот момент и пошёл на улицу. Выйдя на улицу, я почувствовал покусывания в ногах, закатав колоши, я увидел кучу блох (14 штук). Изучив квартиру, я обнаружил, что они обитают в определенном месте в доме, которое находится далеко от комнаты, где я сплю, но чтобы выйти на улицу, я должен пересечь их логово. В общем, большую часть времени я находился в безопасной комнате (и блох на мне действительно в это время не появлялось), а когда нужно было выйти на улицу, я быстро пробегал через блохастую комнату, иногда даже выходя на улицу не подцепив ни одной блохи, но чаще всего 1-2 все же цеплялись. Периодически я созванивался с матерью и в одном из разговоров я рассказал про блох. Пообщавшись, мы с матерью договорились, что я возвращаюсь домой, но 3 месяца не ищу работу, а буду писать компилятор и в это время она не будет меня донимать. Вернувшись домой, я менее чем за 3 месяца дописал компилятор. Прежде чем продолжить, я хочу вам рассказать о подходе который я использую при разработке. Свой подход я со временем выработал сам и старался всегда его придерживаться(хотя иногда меня заносит и я забываю его использовать).

Подход заключается в следующем: я представляю, что существует библиотека со всеми возможными типами и функциями, кроме одной, той самой которую мне сейчас надо написать. Я пишу эту недостающую функцию (используя всю мощь функций и типов из выдуманной библиотеки), затем запускаю компилятор и если не хватает типа, создаю его, а если не хватает функции, то эта функция становится той самой которую мне необходимо написать. Сразу скажу, что сейчас я изменил свой подход, и вместо представления о том, что в библиотеке есть функции и типы, я представляю, что там есть классы и методы (даже если язык не объектно ориентированный). Когда я закончил компилятор, разумеется в нем было куча ошибок. Я начал поиск и исправление ошибок, но из-за большого количества возможностей(дженерики, параметры аргументов функций, классы типов, функции высшего порядка), ошибок было крайне много, а из-за подхода который я использовал при разработке, изменение одной функции могло приводить к изменению большой группы функций. Долгое время исправляя ошибки(а к слову говоря, больше чем искать ошибки, я ненавижу только писать юнит тесты), я наткнулся на ошибку для исправления которой необходимо переписать 60% кода. Я впал в отчаянье и думал, как мне мне сделать так, чтобы язык был простым, но поддерживал все мне необходимое. Я решил попробовать взять за основу ООП и для того, чтобы язык был простым, придерживаться следующих правил:

  1. В языке должны быть только классы, свойства и методы(ничего больше).
  2. Каждый метод, в конкретном классе должен компилироваться в одну LLVM IR функцию. То есть, в классе A есть метод m, который в не зависимости от того какие аргументы ему будут переданы, всегда компилируется в единственную LLVM IR функцию, но если B наследует A, то метод m уже компилируется в другую LLVM IR функцию.
  3. Управление памятью должно быть автоматическим.
  4. Язык не должен иметь ссылок.

Я приступил к обдумыванию, как сделать необходимые для языка возможности не нарушая правила. И вот, что я придумал:

  • Как сделать аналог классов типов из Haskell?

    Создать абстрактные классы, которые могут иметь методы, которые при наследовании в не абстрактный класс необходимо перезаписать. Пример:

    abstract class Printable
    
    require method (Printable) print()
    method (Printable) printLn()
        this.print()
        String.eol().print()
    

  • А если нужно унаследовать от абстрактного класса существующий класс?

    Просто разрешить это делать, при условии, что существующий класс перезапишет методы, требуемые абстрактному классу. Пример:

    class Some(SomeParent)
        a, b String
    
    class Some(Some, Printable)
    
    override method (Some) print()
        this.a.print()
        this.b.print()
    

  • Как отказаться от ссылок?

    Работать с переменными только как со значениями(но 2 переменные могут содержать одно и то же значение, изменяя которое в одной переменной — изменяет и во второй).
  • А как без указателей передавать аргументы в функцию, которая должна менять эти аргументы?

    Помечать какие аргументы можно изменять, а какие нет. На внутреннем уровне изменяемые аргументы будут передаваться как ссылки на эти аргументы. Пример:

    method (Some) foo(a :String, b String) // a - можно изменять, b - нет
        c := 10 // c можно изменять
        d .= 12 // d нельзя изменять
        с:someMethod() // вызывается метод someMethod, c передается как изменяемый объект
        c.someMethod() // вызывается метод someMethod, c передается как неизменяемый объект
        //2 метода выше, хоть и имеют одно имя, являются двумя разными методами, которые могут иметь разные сигнатуры
    

  • Как сделать дженерики?

    При указании какого класса является объект, можно указать видоизмененный класс. Пример:

    class Point
        x,y Number
    
    method (Some) foo(a Point<x UInt64, y UInt64>) //аргумент a является экземпляром класса, который является наследником класса Point, но с измененными классами свойств x и y. Разумеется новый класс свойства, должен быть наследником старого класса.
    
  • Логично, чтобы в классе Point свойства x и y всегда имели один тип.

    class Point
        x, y !Number // класс свойства x можно менять, а y всегда как x
    

  • А если свойства приватные?

    Сделать не читаемые свойства, в которые нельзя записывать или читать из них значения, но можно узнавать какого они класса. Пример:

    class Point
        numberClass #Number
        private x, y .numberClass
    
    method (Point) createPoint(x, y .numberClass) This //This - класс такой же как и у объекта к которому применяется метод
        result:x = x
        result:y = y
    

  • Как сделать так, чтобы любой метод конкретного класса имел только один вариант LLVM IR функции?

    Использовать в качестве классов аргументов методов указатели на свойства (как выше) или указывать конкретные классы, которые не меняются при наследовании. Пример:

    method (Some) foo(a .z, b Point<numberClass .f>, c UInt64)
    //a имеет класс как у свойства z класса к которому применяется метод
    //b имеет класс который наследуется от класса Point но с измененным классом свойства numberClass, новый класс свойства имеет такой же класс как и свойство f класса к которому применяется метод
    //c всегда имеет класс UInt64, изменить его при наследовании нельзя
    

  • Как реализовать автоматическое управление памяти?

    При выделении памяти на куче, к этой памяти добавлять счётчик, которые увеличивается, если какой-то объект использует эту память и уменьшается, если какому либо объекту, эта память не нужна. Если счётчик достиг 0 — освобождать память.

Концепт выглядит неплохо, приступил к разработке с нуля. Мои три месяца прошли и пришла пора искать работу. Время тяжёлое, работы нигде нет, ходят слухи, что даже работников отдела кадров, заставляют печатать фальшивые вакансии, чтобы создать видимость, что не всё так плохо. И я верю этим слухам, так как, часто звонишь по объявлению, а мне сразу говорят — вакансия не актуальна, хотя вакансия регулярно обновляется и висит на протяжении долгого времени. Поиск хоть какой-нибудь работы занял 5 месяцев. Устроился я в департамент охраны на должность контролёр КПП, на объект находящийся в туберкулёзной больнице. В мои обязанности входило: досмотр больных и посетителей. Зарплата — 170 $/мес. Несмотря на то, что часов работы стало больше, чем на предыдущих работах, времени на разработку тоже стало больше, поскольку работа была посменная. И вот я дописал весь код, создал стандартную библиотеку, написал для библиотеки тесты.

Запустив тесты я увидел множество ошибок, в процессе их исправления я понял, что большинство из них связаны с неопределенным поведением, а устранять их крайне тяжело. Например некоторые ошибки проявлялись через раз, некоторые проявлялись на уровне оптимизации O0 и O2, но не проявлялись на O1 и O3. Конца и края ошибкам я не видел, а толкового инструментария для поиска ошибок в LLVM IR, я не нашёл. Во время изучения мною LLVM IR, я смотрел, что генерирует clang из C. В какой-то момент я понял, что превращение из C в LLVM IR занимает крайне мало времени, а основную часть занимает оптимизация LLVM IR. Так зачем я использую LLVM IR? Ведь его оптимизация все равно будет происходить на компьютере конечного пользователя.

Так почему мне не использовать C вместо LLVM IR? Я вижу только плюсы при замене LLVM IR на C: нормальные макросы, упрощение написания кода, кучу инструментария, перспектива при необходимости использовать сторонние C библиотеки. И я начал переписывать компилятор заменяя LLVM IR на C. Когда я закончил замену, я быстро нашёл и исправил все ошибки найденные в тестах. Во время написания компилятора самой распространённой моей ошибкой было изменение какого либо значения по указателю, думая, что он уникален, а на самом деле на объект по указателю указывало несколько ссылок. Я решил изменить количество уровней мутабельности переменных, this объектов и аргументов методов с 2 (мутабельные и иммутабельные) до 4. В этой статье опущено множество деталей, так как объяснение всех нюансов и тонкостей на всех этапах создания языка, заняло бы 20 — 40 таких статей (которая и так получается огромной). Но для того, чтобы понять новые уровни, все таки необходимо уточнить некоторые детали. Когда я писал про то, что 2 переменные могут содержать одни и те же данные и изменяя одну переменную, вы можете повлиять на вторую, возможно вы подумали, что при создании переменной, в куче создаётся объект, а переменная хранит указатель на этот объект, но это не так. Я очень хотел, чтобы программы на моём языке были производительны, а выделять память под каждую переменную — далеко не производительно. Поэтому при создании переменной значение хранится в переменной, но в значении может быть указатель. При присвоении переменной какого либо объекта, значения объекта копируются в перемененную и все указатели указывают на одну и туже область памяти.

А теперь вернемся к уровням мутабельности, вот два новых уровня:

  • Можно менять значение переменной, но нельзя менять значения по ссылкам.
  • Нельзя менять значение переменной, но можно менять значения по ссылкам.

Возможно у вас возник вопрос — «Зачем учитывать ссылки, если язык их не поддерживает?». Отвечаю, их не нужно было учитывать, просто нужно было придерживаться нескольких правил, нарушая которые в большинстве случаев, была бы ошибка компиляции. Также мною была добавлена возможно указывать, что this объект в методе будет не читаем (по аналогии со свойством). Язык стал выглядеть так:

method (#Some) foo(a String, b `String, c ~String, d :String)
//this нельзя использовать
//a можно только читать
//в b можно заменить любой символ, но нельзя присвоить новую строку или изменить длину строки
//c может присвоить новую строку и изменить длину строки, но нельзя изменить символы в уже переданной строке
//d можно изменить, что угодно

Снова переписал компилятор и начал перепись стандартной библиотеки. Во время переписи обнаружил серьёзную проблему, дело в том, что если при вызове метода, аргумент которого (по сигнатуре) частично, либо полностью должен быть иммутабельным, но будет передан более мутабельный аргумент, то уровень его мутабельности для вызова снизится (что логично) и вот в чём проблема: предположим, что у нас есть полностью мутабельная переменная и мы применяем к ней метод, который применяется только к переменой с мутабельным значением, но с иммутабельными значениями по ссылкам, в таком методе будет легально присвоить в this полностью иммутабельный объект (так как значения копируются, а ссылки и там и там иммутабельны), но при завершении метода, мы получим полностью мутабельную переменную, в которой есть ссылка на иммутабельное значение, а это полностью всё ломает. Объяснение получилось достаточно запутанным, поэтому вот пример:

method (~Some) veryBadMethod(value This)
 this = value

method (#Some) foo()
    a := Some
    b .= Some#randomValue() //подразумевается, что с использованием b не возможно ничего изменить
    a~veryBadMethod(b)
    a:someMethod() //а вот здесь, вышеуказанное правило нарушается

В тот момент когда я осознал проблему, я был в бешенстве. Я честно пытался создать безопасный, простой и обязательно создающий производительные программы язык, но всё тщетно. Все, надоело, плевать на производительность, рубисты же как то выживают. Решил — будет только 2 уровня мутабельности, как в начале, а при присвоении какого либо объекта, его значение полностью копируется, в этом случае ещё не надо париться насчёт управления памятью, объект не нужен — значит полностью его освободить.

Во время очередной переписи компилятора, я начал очень сильно кашлять, сопровождалось всё это высокой температурой. Кашель был настолько сильным, что я подумал «Все! Молодец! Поработал в туберкулёзной больнице.». Я пошёл в поликлинику, где мне сделали снимок лёгких и положили в больницу. Оказалось — острый бронхит. Во время пребывания в больнице, мне постоянно меряли давление, верхняя граница которого частенько была в диапазоне 150-180. Количество таблеток от давления и от бронхита в сумме составляла 11 штук в день. После выхода из больницы, у меня начались проблемы с когнитивными способностями, особенно это касалось памяти, у меня были такие ситуации когда я о чём-то думал и в процессе думания, забывал о чём и долго не мог вспомнить. В больницу я по этому поводу не пошёл (почему? в двух словах не рассказать, статью целую написать можно). Со временем мои способности начали восстанавливаться, но полностью до сих пор не восстановились. Вернувшись с больницы, плохо соображающий, почти 30-летний парень, живущий с матерью, на окраине мира, за 200$ в месяц (немного увеличилась зарплата), потративший свою молодость на то, что так и не смог сделать, на меня накатилось крайне депрессивное состояние.

Я начал прокрастинировать, в основном играя в компьютерные игры. В одной из игр (Disciples 3) был такой юнит — верховный вампир, когда он изрядно ранен, он становится гораздо сильнее и произносит фразу — «Погибну сражаясь!». Эта фраза меня вдохновила, ведь какой смысл был в моей жизни, если я не закончу то, что начал? Я закончу свой компилятор, чего бы мне это не стоило! Я продолжил разработку, причём если раньше я писал только по выходным, то теперь я частенько стал писать и после работы. Практически сразу после начала разработки, я подумал «Зачем я полностью копирую объекты? Ведь я могу сделать как в Haskell.». Если хаскелю дать сложить 10 чисел, то он их не сложит, а лишь пометит себе, что их надо сложить, но непосредственно сложение будет произведено в момент когда понадобится результат вычисления, а такое событие может и не произойти. Я могу сделать так-же, не копировать объекты, а пометить сколько объектов владеет областью памяти (тем более у меня есть счетчики, те самые которые обеспечивают автоматическое управление памятью) и если памятью владеет несколько объектов, и значения в этой памяти нужно изменить, то только тогда она будет скопирована.

И тут я понимаю, что всё ещё могу сделать программы на моём языке производительными. И вот настал момент когда я все закончил, но для меня было принципиально важно переписать компилятор, используя свой язык. Во время переписывания компилятора, я сразу понял, что язык крайне неудобен, особенно мне мешали ссылки на свойства, они задумывались как полезная фишка языка, а на практике оказались самым большим его недостатком и никто в здравом уме не будет использовать такой язык.

В тот момент когда в языке было множество вариантов мутабельности, я подумывал над ещё одним дизайном языка, но не стал его реализовывать, так как при стольких вариантах мутабельности он был крайне неудобен, а то, что переменные могут быть взаимосвязаны, делало бы язык небезопасным. Но теперь уже таких проблем нет. И я начал всё заново. Новый язык больше не объектно ориентированный, вместо классов теперь типы, с возможностью создавать дженерики. Пример:

type Point(a)
    x,y a

type SimplePoint
    x,y Double

//где-то в коде
a := Point(UInt64)

Есть аналог класса типов из Haskell, в моём языке он называются группой, но в отличии от класса типов, в группе не может быть каких либо ограничений на то, какой тип может быть членном группы. Логически они есть, но из-за особенностей языка их нельзя реализовать. Поэтому требования к группе я пишу в комментариях при создании группы. Пример:

group Equal
//equal(a, a) -> Bool

То есть, если тип является членом группы Equal, то должна быть функция с именем equal, которая должна принимать 2 аргумента данного типа, и возвращать тип Bool.

В языке появились правила (rules) — это такой мини язык программирования, код которого выполняется во время компиляции. В этом коде можно добавить тип в группу. Пример:

rules
    1 = type == [_] //если некий тип является слайсом
    2 = type[0] > Equal //а его элементы входят в группу Equal
    3 = 1 & 2
    join 3 Equal //то добавить такой тип в группу Equal

Теперь может быть бесконечное множество функций и процедур с одинаковым именем. Для того, чтобы определить какую функцию из множества доступных вызвать, в функциях так же есть правила, которые оперируя типами передаваемых аргументов, могут определить — подходит ли функция к данным аргументам. У функций есть приоритеты, от 1 до 9 (по умолчанию 5), если при вызове функции, есть несколько подходящих варианта с одинаковым приоритетом, и нет подходящего варианта с приоритетом повыше, то это ошибка компиляции. Пример функции:

func notEqual(a, b)
    rules
        final = a > Equal & a == b
        result = Bool
    result = !(a == b) //! и == синтаксический сахар, данное выражение эквивалентно result = (neg(equal(a, b)) или a.equal(b).neg()

Закончив разработку нового компилятора, переписав стандартную библиотеку и тесты для неё, я начал переписывать компилятор на собственном языке, как я писал ранее, для меня это принципиально. Я очень боялся, что снова обнаружу проблему в языке, которую не смог предусмотреть заранее, но все прошло гладко, а писать на таком языке, было сплошным удовольствием. Для сборки программ, я использовал самописные bash скрипты, но разумеется это не вариант. Поэтому я написал(разумеется на своём языке) сборщик и установщик пакетов.

И вот наконец, я готов представить вашему взору: язык программирования cine, компилятор с таким же именем, сборщик и установщик проектов fei. Вот ссылки на github:

Инсталятор cine и fei.
Исходники компилятора.
Исходники установщика и сборщика пакетов.
Исходники стандартной библиотеке(я использую название модуль).
XML файл для создания подсветки синтаксиса в текстовом редакторе Kate.

Для установки и работы вам необходим Linux x86_64(к сожалению пока только так) и установленный clang.

Поскольку никаких учебников по языку ещё не существует, я открыл блог, где буду писать туториалы по программированию на cine.

Интересные факты:

  • Названия cine и fei произошли от сгенерированно самописным генератором случайных имен n-kine и enfei.
  • В коде на языке cine можно использовать код написанный на C.
  • Компилятор cine автоматически сортирует члены типов по выравниванию (от большего к меньшему).
  • Язык изначально назывался 4ernika, так как в момент когда я решил его создать, я ел чернику.
  • Когда язык был объектно ориентированным, была возможность при наследовании класса от нескольких классов, объединять их свойства. Например size и count объединить в одно свойство, то есть при обращении к свойству size или count, обращение происходило к одному и тому же свойству. Так же была возможность наоборот, создать класс в котором одно свойство имело несколько имен, а при наследовании, этим именам можно было назначить разные свойства.
  • С самого начала и до конца, в языке не было глобальных переменных (отказался от них ещё в лицее).
  • Во время написания одной из версий языка, я придумал новый алгоритм обучения нейронных сетей основанный на новом виде чисел, которые находятся над комплексными (все комплексные числа можно представить в виде новых чисел, но не все новые числа можно представить в виде комплексных). Мне очень хотелось попытаться реализовать этот алгоритм и попробовать его в деле, но поскольку это заняло бы немало времени, я сфокусировался на компиляторе. Но рано или поздно вернусь к этому алгоритму.
  • В языке cine нет специального типа для строк, строка в cine — слайс из элементов типа UInt8, но в каждом слайсе есть специальный флаг, которым помечаются строки.
  • Когда я перешел от использования LLVM IR к C, встал выбор об использовании clang или GCC. Написав несколько примеров, которые скомпилировал двумя компиляторами в ассемблерный код, просмотрев оба файла, я остановился на clang.
  • Хотя изначально язык затевался с целью возможности включения SIMD инструкций в код, в версии 0.1 нет явного использования SIMD (оно обязательно появится), но в одном из проходных вариантов языка, я не выдержал и реализовал поиск элементов слайса с использованием SIMD инструкций.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 127: ↑111 и ↓16+127
Комментарии525

Публикации

Истории

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань